mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-16 18:12:29 +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 —
|
Read `/tmp/ai-fix/batch.json`. Work add-on by add-on, not issue by issue —
|
||||||
grouping is the point of the batch.
|
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
|
## Hard limits
|
||||||
|
|
||||||
These are not guidelines. A workflow step enforces them after you finish, and
|
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.
|
will not be around to resolve.
|
||||||
3. **One add-on per branch, one branch per pull request.** Branch name
|
3. **One add-on per branch, one branch per pull request.** Branch name
|
||||||
`ai-fix/<addon>-<issue-number>`.
|
`ai-fix/<addon>-<issue-number>`.
|
||||||
4. **Draft pull requests only.** Never merge, never mark ready for review,
|
4. **Never merge, never close an issue, never enable auto-merge.** Opening a
|
||||||
never close an issue.
|
pull request for review is as far as you go — a human ships it. CI
|
||||||
5. If the fix requires changing more than roughly 60 lines, or touching more
|
(`onpr_check-pr.yaml`: lint + Docker build) runs on every pull request you
|
||||||
than three files, stop. Post the analysis, open no pull request, and say
|
open and is what actually validates the change.
|
||||||
plainly that the change is too large for an unattended fix.
|
5. **The small-fix ceiling is ~60 changed lines / 3 files.** A change under it,
|
||||||
6. **Relabel every issue before moving to the next one.** This sweep runs
|
*and* with a root cause you are confident of, may go out as a ready pull
|
||||||
daily over the same `ai-triage`-labelled backlog. An issue you have
|
request (Outcome A). Anything over it does not become a pull request — it
|
||||||
already finished with must drop that label immediately — otherwise
|
becomes a plan (Outcome B), however confident you are.
|
||||||
tomorrow's sweep re-selects it, opens a second competing branch, and
|
6. **Relabel every issue before moving to the next one** (see "Relabel", below).
|
||||||
burns another full read-the-source-and-fix pass on work that is already
|
This sweep runs daily over the same `ai-triage` backlog; an issue you have
|
||||||
done. Do this as your last action on the issue, right after commenting,
|
finished with must drop that label immediately or tomorrow's sweep re-selects
|
||||||
not batched at the end: `gh issue edit <n> --remove-label ai-triage
|
it and burns another full pass on work already done.
|
||||||
--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.
|
|
||||||
|
|
||||||
## Per add-on, do this in order
|
## 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.
|
than reading the whole tree.
|
||||||
|
|
||||||
**2. Establish the root cause, and be honest about confidence.** Name the exact
|
**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
|
file and line. If you cannot, you have a hypothesis, not a root cause. Do not
|
||||||
must label it as such in the comment. Do not dress a guess up as a diagnosis.
|
dress a guess up as a diagnosis — Alex has to trust these without re-deriving
|
||||||
Alex has to trust these comments without re-deriving them.
|
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
|
**3. Re-check the upstream/wrapper split.** Tier 1 made this call cheaply,
|
||||||
it made it cheaply and without reading the source. If the real fault is
|
without reading the source. If the real fault is upstream, say so, open no pull
|
||||||
upstream, say so, do not open a pull request, and suggest what to file with the
|
request, and suggest what to file upstream instead. Reversing tier 1 is a
|
||||||
upstream project instead. Reversing tier 1's classification is a correct and
|
correct and valuable outcome, not a failure.
|
||||||
valuable outcome, not a failure.
|
|
||||||
|
|
||||||
**4. Fix it.** Match the surrounding style — this repo is bash and Dockerfiles,
|
## Decide the outcome
|
||||||
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.
|
|
||||||
|
|
||||||
**5. Open the draft pull request.** Body must contain: the root cause with file
|
Pick exactly one per issue. When you are between two, pick the more cautious
|
||||||
and line, what the change does, how you verified it (or an explicit statement
|
(A→B→D): a plan a human approves in one click costs far less trust than a wrong
|
||||||
that you could not verify it), and `Closes #<n>`.
|
pull request.
|
||||||
|
|
||||||
**6. Comment on the issue, then relabel it (hard limit 6).** Root cause, the
|
### Outcome A — ready pull request → `ai:fixed`
|
||||||
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
|
**Only when both hold:** you named the root cause to an exact file and line and
|
||||||
what you ruled out and what you would need to go further. Close with a note
|
are genuinely confident of it, **and** the fix is within the small-fix ceiling
|
||||||
that this is automated analysis pending Alex's review. Then remove
|
(rule 5).
|
||||||
`ai-triage` and add the one replacement label that matches the outcome,
|
|
||||||
before starting the next issue.
|
- 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
|
## Meta-findings
|
||||||
|
|
||||||
This is the part a per-issue run cannot do, so do not skip it.
|
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
|
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
|
cause — one base image bump, one s6 change, one upstream release, one bad option
|
||||||
option default replicated across add-ons — open a single issue titled
|
default replicated across add-ons — open a single issue titled `[meta] <pattern>`
|
||||||
`[meta] <pattern>` describing it, linking the affected issues, and proposing
|
describing it, linking the affected issues, and proposing the systemic fix
|
||||||
the systemic fix rather than the individual patches.
|
rather than the individual patches.
|
||||||
|
|
||||||
Report honestly if the batch produced nothing. A sweep that fixes zero issues
|
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
|
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
|
# Destination: .github/workflows/daily_ai_fix.yaml
|
||||||
#
|
#
|
||||||
# Tier 2. One Opus 4.8 run at xhigh over the whole batch of `ai-triage` issues,
|
# Tier 2 of the AI triage system. One Opus 4.8 run at xhigh over the whole
|
||||||
# grouped by add-on, so it can spot the cross-issue patterns a per-issue run
|
# batch of `ai-triage` issues, grouped by add-on, so it can spot the cross-issue
|
||||||
# never sees ("these four reports are all the same base image bump"). Runs
|
# patterns a per-issue run never sees ("these four reports are all the same base
|
||||||
# daily rather than weekly, so batches (default limit 8) stay small and any
|
# image bump"). Runs daily rather than weekly, so batches (default limit 8) stay
|
||||||
# one day's failure doesn't block a week's worth of issues.
|
# 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:
|
# Auth:
|
||||||
# Claude Pro/Max subscription via the CR_PAT GitHub Environment, which
|
# Claude Pro/Max subscription via the CR_PAT GitHub Environment, which
|
||||||
@@ -41,6 +58,9 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sweep:
|
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
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 180
|
timeout-minutes: 180
|
||||||
environment: CR_PAT
|
environment: CR_PAT
|
||||||
@@ -100,7 +120,7 @@ jobs:
|
|||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
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
|
gh label create "$l" --repo "$REPO" --color ededed --force >/dev/null 2>&1 || true
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -109,6 +129,9 @@ jobs:
|
|||||||
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
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: |
|
prompt: |
|
||||||
The batch of issues to work through is /tmp/ai-fix/batch.json.
|
The batch of issues to work through is /tmp/ai-fix/batch.json.
|
||||||
Follow .github/prompts/issue-fix.md exactly. Do not deviate from
|
Follow .github/prompts/issue-fix.md exactly. Do not deviate from
|
||||||
@@ -147,29 +170,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
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
|
- name: Guard forbidden paths
|
||||||
if: always() && steps.batch.outputs.count != '0'
|
if: always() && steps.batch.outputs.count != '0'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||||
REPO: ${{ github.repository }}
|
REPO: ${{ github.repository }}
|
||||||
run: |
|
run: bash .github/scripts/ai_guard_paths.sh
|
||||||
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
|
|
||||||
|
|||||||
5
.github/workflows/daily_stale.yml
vendored
5
.github/workflows/daily_stale.yml
vendored
@@ -29,5 +29,8 @@ jobs:
|
|||||||
stale-pr-label: 'stale'
|
stale-pr-label: 'stale'
|
||||||
days-before-stale: '5'
|
days-before-stale: '5'
|
||||||
days-before-close: '2'
|
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
|
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
|
||||||
118
.github/workflows/on_issues_ai_triage.yaml
vendored
118
.github/workflows/on_issues_ai_triage.yaml
vendored
@@ -1,9 +1,23 @@
|
|||||||
---
|
---
|
||||||
# Destination: .github/workflows/on_issues_ai_triage.yaml
|
# Destination: .github/workflows/on_issues_ai_triage.yaml
|
||||||
#
|
#
|
||||||
# Tier 1. Fires on every new issue, costs cents, finishes in ~2 minutes.
|
# Tier 1 of the AI triage system. Fires on every new issue, costs cents,
|
||||||
# Classifies, de-duplicates, asks for missing info, answers simple questions,
|
# finishes in ~2 minutes on Sonnet-low. Classifies, de-duplicates, asks for
|
||||||
# and applies the `ai-triage` label that tier 2 (daily_ai_fix.yaml) picks up.
|
# 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
|
# Auth: Claude Pro/Max subscription via the CR_PAT GitHub Environment, which
|
||||||
# holds the CLAUDE_CODE_OAUTH_TOKEN secret (generate with `claude setup-token`).
|
# holds the CLAUDE_CODE_OAUTH_TOKEN secret (generate with `claude setup-token`).
|
||||||
@@ -13,6 +27,11 @@ name: AI issue triage
|
|||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened]
|
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:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
issue:
|
issue:
|
||||||
@@ -25,7 +44,7 @@ permissions:
|
|||||||
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
|
||||||
|
|
||||||
concurrency:
|
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
|
cancel-in-progress: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@@ -33,18 +52,45 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
classify:
|
classify:
|
||||||
# Manual dispatch is a deliberate override: skip the auto-trigger guards
|
# Three entry paths:
|
||||||
# (don't self-triage the maintainer's own issues; honour the no-ai
|
# * issues.opened — the normal fire-on-every-open path, with the guards
|
||||||
# opt-out) that only make sense for the fire-on-every-open path.
|
# 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: >-
|
if: >-
|
||||||
|
vars.AI_DISABLED != 'true' &&
|
||||||
|
(
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
(github.event.issue.user.login != 'alexbelgium' &&
|
(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'))
|
!contains(github.event.issue.labels.*.name, 'no-ai'))
|
||||||
|
)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
environment: CR_PAT
|
environment: CR_PAT
|
||||||
|
|
||||||
steps:
|
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
|
# on_issues_ping_submitter.yml has to land first: the classifier reads
|
||||||
# the existing comments and bails out if someone already owns the issue.
|
# the existing comments and bails out if someone already owns the issue.
|
||||||
# Both workflows fire on the same issues.opened event and race. The
|
# Both workflows fire on the same issues.opened event and race. The
|
||||||
@@ -76,7 +122,7 @@ jobs:
|
|||||||
- name: Classify
|
- name: Classify
|
||||||
id: classify
|
id: classify
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: anthropics/claude-code-action@v1
|
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
|
||||||
with:
|
with:
|
||||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
show_full_output: true
|
show_full_output: true
|
||||||
@@ -126,7 +172,8 @@ jobs:
|
|||||||
if [ "$VERDICT" = "owned" ]; then
|
if [ "$VERDICT" = "owned" ]; then
|
||||||
echo "issue already has an owner, nothing to do"
|
echo "issue already has an owner, nothing to do"
|
||||||
gh issue edit "$ISSUE" --repo "$REPO" \
|
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
|
>/dev/null 2>&1 || true
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -145,6 +192,14 @@ jobs:
|
|||||||
if [ "$VERDICT" = "addon-bug" ] && [ "$CONF" != "low" ]; then
|
if [ "$VERDICT" = "addon-bug" ] && [ "$CONF" != "low" ]; then
|
||||||
LABELS+=("ai-triage")
|
LABELS+=("ai-triage")
|
||||||
fi
|
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")
|
LABELS+=("ai:classified")
|
||||||
|
|
||||||
# No --force: an existing label (e.g. a model-supplied cosmetic
|
# No --force: an existing label (e.g. a model-supplied cosmetic
|
||||||
@@ -166,7 +221,7 @@ jobs:
|
|||||||
declare -A FRESH=()
|
declare -A FRESH=()
|
||||||
for l in "${LABELS[@]}"; do FRESH["$l"]=1; done
|
for l in "${LABELS[@]}"; do FRESH["$l"]=1; done
|
||||||
STALE=()
|
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")
|
[ -z "${FRESH[$l]:-}" ] && STALE+=("$l")
|
||||||
done
|
done
|
||||||
if [ "${#STALE[@]}" -gt 0 ]; then
|
if [ "${#STALE[@]}" -gt 0 ]; then
|
||||||
@@ -183,3 +238,44 @@ jobs:
|
|||||||
} > /tmp/ai-triage/comment.md
|
} > /tmp/ai-triage/comment.md
|
||||||
gh issue comment "$ISSUE" --repo "$REPO" --body-file /tmp/ai-triage/comment.md
|
gh issue comment "$ISSUE" --repo "$REPO" --body-file /tmp/ai-triage/comment.md
|
||||||
fi
|
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.
|
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
|
## Linting Rules
|
||||||
|
|
||||||
| Tool | Config | Key ignores |
|
| Tool | Config | Key ignores |
|
||||||
|
|||||||
Reference in New Issue
Block a user