Skip to content

Triage: AI

Triage: AI #6

Workflow file for this run

name: "Triage: AI"
# AI-assisted issue triage using GitHub Models (actions/ai-inference).
# On a new issue it asks a model to classify the issue against the AmbleKit
# label taxonomy, then applies labels, sets the issue type, sets the org
# Priority/Effort fields (best effort), and leaves a triage comment that
# includes a likely-cause (bugs) / recommended-implementation (features) note.
# If the model output can't be parsed it falls back to "S: Untriaged".
on:
issues:
types: [opened, reopened]
workflow_dispatch:
inputs:
issue:
description: "Issue number to (re)triage"
required: true
permissions:
issues: write
models: read
contents: read
concurrency:
# key off trusted contexts only - issue number for issue events, run_id for
# manual runs (the dispatch input is user-controlled, don't put it in the key)
group: triage-${{ github.event.issue.number || github.run_id }}
cancel-in-progress: true
jobs:
triage:
runs-on: ubuntu-latest
steps:
- name: Resolve issue number
id: ctx
env:
NUM: ${{ github.event.issue.number || github.event.inputs.issue }}
run: |
# NUM can come from a user-controlled workflow_dispatch input - require
# digits only so it can't inject newlines into $GITHUB_OUTPUT.
if ! printf '%s' "$NUM" | grep -qE '^[0-9]+$'; then
echo "invalid issue number: $NUM" >&2; exit 1
fi
echo "num=$NUM" >> "$GITHUB_OUTPUT"
- name: Fetch issue (for manual runs) + build prompt
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
NUM: ${{ steps.ctx.outputs.num }}
EVENT_TITLE: ${{ github.event.issue.title }}
EVENT_BODY: ${{ github.event.issue.body }}
run: |
set -euo pipefail
if [ -n "${EVENT_TITLE:-}" ]; then
TITLE="$EVENT_TITLE"; BODY="$EVENT_BODY"
else
TITLE=$(gh issue view "$NUM" --repo "$REPO" --json title --jq '.title')
BODY=$(gh issue view "$NUM" --repo "$REPO" --json body --jq '.body')
fi
{
echo "Issue #$NUM"
echo "Title: $TITLE"
echo
echo "Body:"
echo "${BODY:-(no body)}"
} > "$RUNNER_TEMP/triage-prompt.txt"
- name: AI classify
id: ai
# don't fail the run if GitHub Models is unavailable (not enabled / rate-limited
# / 403) - the Apply step falls back to S: Untriaged when the response is empty.
continue-on-error: true
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1
with:
model: openai/gpt-4o-mini
max-tokens: 800
prompt-file: ${{ runner.temp }}/triage-prompt.txt
system-prompt: |
You triage GitHub issues for Timeless Heroes - a Fabric 1.20.1 Minecraft
*content* mod (Java, mod id "timeless") that adds superhero suits with
custom powers and abilities.
Architecture you can reason about for the "insight" field:
- Suits live in suit/ (Iron Man marks mk2/3/5/7/50/85, Spider-Man, Batman,
Moon Knight, Superman). A Suit owns a PowerList; a "set" equips the four
armour pieces. Per-player state is NBT via SuitItem.Data (auto-synced).
- Powers live in power/impl/, registered in PowerRegistry, identified by
Identifier. They have run/tick(server), tick(client), and client hooks
(handlesRightClick/LeftClick, applyPose). 9 keybinds send UsePowerC2SPacket.
- Rendering: SuitFeature draws the suit model over the player; SuitModel
dispatches power poses; nano assembly uses a custom core shader. Animation
system under mc/duzo/animation. Jarvis-style HUD in client/gui.
- Networking is c2s/s2c FabricPacket records in network/. Datagen in datagen/.
Classify the issue and reply with ONE LINE of minified JSON, no code
fences, no prose. Schema (use EXACTLY these allowed values):
{
"type": "Bug" | "Feature" | "Task",
"priority": "Urgent" | "High" | "Medium" | "Low",
"effort": "High" | "Medium" | "Low",
"difficulty": "DB" | "D3" | "D2" | "D1" | "D0",
"areas": [ subset of: "A: Suits","A: Powers","A: Entities","A: Datagen","A: Admin Tooling","A: Core Tech","A: Build","A: Docs" ],
"changes": [ subset of: "C: Render","C: UI","C: Networking","C: Textures","C: Audio","C: Translations","C: Structures","C: No Java" ],
"summary": "one short lowercase sentence",
"insight": "for a Bug: 2-4 sentences on the most likely cause, naming the suspect subsystem/class/package; for a Feature: 2-4 sentences on a recommended implementation approach, pointing at where in the codebase it would go; for a Task: empty string",
"needs_info": true | false
}
Guidance: crashes / data loss -> priority High or Urgent. A vague report
with no repro/logs -> needs_info=true. "Task" = docs/chores/proposals,
"Feature" = new suit/power/content, "Bug" = something broken. difficulty
is how much codebase knowledge a fix needs (DB beginner .. D0 extreme).
Pick the 1-2 most relevant areas; do not invent labels outside the lists.
Keep "insight" concrete but clearly a best guess - a maintainer confirms it.
- name: Apply triage
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
NUM: ${{ steps.ctx.outputs.num }}
RESP: ${{ steps.ai.outputs.response }}
# optional overrides - set as repo/org Actions variables instead of editing
# this file. find the ids via: gh api orgs/<org>/issue-fields
PRIORITY_FIELD_ID: ${{ vars.PRIORITY_FIELD_ID }}
EFFORT_FIELD_ID: ${{ vars.EFFORT_FIELD_ID }}
run: |
set -uo pipefail
# prefer the whole response if it already parses as json (expected case:
# the model is asked for one line of minified json). otherwise strip any
# code fences and fall back to the first {...} block.
if printf '%s' "$RESP" | jq -e . >/dev/null 2>&1; then
JSON="$RESP"
else
JSON=$(printf '%s' "$RESP" | tr -d '\r\n' | sed 's/```json//g; s/```//g' | grep -o '{.*}' | head -1 || true)
fi
echo "model json: $JSON"
if [ -z "$JSON" ] || ! printf '%s' "$JSON" | jq -e . >/dev/null 2>&1; then
echo "no valid json -> S: Untriaged"
gh issue edit "$NUM" --repo "$REPO" --add-label "S: Untriaged" || true
exit 0
fi
# a previous run may have left S: Untriaged - clear it now that we have a
# valid classification so the issue isn't both triaged and untriaged.
gh issue edit "$NUM" --repo "$REPO" --remove-label "S: Untriaged" >/dev/null 2>&1 || true
TYPE=$(echo "$JSON" | jq -r '.type // empty')
PRIORITY=$(echo "$JSON" | jq -r '.priority // empty')
EFFORT=$(echo "$JSON" | jq -r '.effort // empty')
DIFF=$(echo "$JSON" | jq -r '.difficulty // empty')
SUMMARY=$(echo "$JSON" | jq -r '.summary // empty' | tr '\r\n' ' ' | cut -c1-200)
INSIGHT=$(echo "$JSON" | jq -r '.insight // empty' | tr '\r\n' ' ' | cut -c1-1200)
NEEDS=$(echo "$JSON" | jq -r '.needs_info // false')
LABELS=()
case "$TYPE" in
Bug) LABELS+=("T: Bugfix");;
Feature) LABELS+=("T: New Feature");;
# Task has no T: label in the taxonomy - it's conveyed by the GitHub
# issue type set further down, so no label is added here on purpose.
esac
case "$PRIORITY" in
Urgent) LABELS+=("P0: Critical");;
High) LABELS+=("P1: High");;
Medium) LABELS+=("P2: Raised");;
Low) LABELS+=("P3: Standard");;
esac
case "$DIFF" in
DB) LABELS+=("DB: Beginner Friendly");;
D3) LABELS+=("D3: Low");;
D2) LABELS+=("D2: Medium");;
D1) LABELS+=("D1: High");;
D0) LABELS+=("D0: Very High");;
esac
# allowlist the model's area/change labels to the documented taxonomy so a
# stray (but repo-existing) label can't sneak in if the model goes off-schema.
declare -A ALLOWED_AC=(
["A: Suits"]=1 ["A: Powers"]=1 ["A: Entities"]=1 ["A: Datagen"]=1
["A: Admin Tooling"]=1 ["A: Core Tech"]=1 ["A: Build"]=1 ["A: Docs"]=1
["C: Render"]=1 ["C: UI"]=1 ["C: Networking"]=1 ["C: Textures"]=1
["C: Audio"]=1 ["C: Translations"]=1 ["C: Structures"]=1 ["C: No Java"]=1
)
while IFS= read -r l; do
[ -n "$l" ] && [ -n "${ALLOWED_AC[$l]:-}" ] && LABELS+=("$l")
done < <(echo "$JSON" | jq -r '((.areas // []) + (.changes // []))[]')
[ "$NEEDS" = "true" ] && LABELS+=("Issue: Awaiting Response")
# only apply labels that actually exist in the repo - page through all of
# them into a set for O(1) membership instead of a nested scan.
declare -A HAVE
while IFS= read -r e; do [ -n "$e" ] && HAVE["$e"]=1; done \
< <(gh api "repos/$REPO/labels" --paginate --jq '.[].name')
ADD=()
for l in "${LABELS[@]:-}"; do
[ -n "${HAVE[$l]:-}" ] && ADD+=("--add-label" "$l")
done
if [ "${#ADD[@]}" -gt 0 ]; then
gh issue edit "$NUM" --repo "$REPO" "${ADD[@]}" || true
fi
# GitHub issue type (best effort - org-level)
if [ -n "$TYPE" ]; then
gh api --method PATCH "repos/$REPO/issues/$NUM" -f type="$TYPE" >/dev/null 2>&1 \
|| echo "issue type not set (token may lack org perms)"
fi
# org Priority/Effort issue fields (best effort)
RID=$(gh api "repos/$REPO" --jq '.id')
set_field() {
[ -z "$2" ] && return 0
# build the payload with jq so the value is properly json-escaped
jq -n --argjson fid "$1" --arg val "$2" \
'{issue_field_values:[{field_id:$fid,value:$val}]}' \
| gh api --method POST "repositories/$RID/issues/$NUM/issue-field-values" --input - >/dev/null 2>&1 \
|| echo "field $1 not set"
}
# ids default to this org's Priority/Effort fields; override via the
# PRIORITY_FIELD_ID / EFFORT_FIELD_ID Actions variables if they differ.
set_field "${PRIORITY_FIELD_ID:-30052706}" "$PRIORITY"
set_field "${EFFORT_FIELD_ID:-30052709}" "$EFFORT"
# triage comment
{
echo "🤖 **auto-triage** (github models)"
echo
echo "| type | priority | effort | difficulty |"
echo "|---|---|---|---|"
echo "| ${TYPE:-?} | ${PRIORITY:-?} | ${EFFORT:-?} | ${DIFF:-?} |"
echo
[ -n "$SUMMARY" ] && echo "> $SUMMARY"
echo
if [ -n "$INSIGHT" ]; then
case "$TYPE" in
Bug) echo "### 🔎 Likely cause";;
Feature) echo "### 🛠️ Recommended implementation";;
*) echo "### 🔎 Insight";;
esac
echo
echo "$INSIGHT"
echo
fi
[ "$NEEDS" = "true" ] && echo "looks like it's missing repro steps / logs - added \`Issue: Awaiting Response\`."
echo
echo "_labels and insight are a best guess, a maintainer will confirm._"
} > "$RUNNER_TEMP/comment.md"
gh issue comment "$NUM" --repo "$REPO" --body-file "$RUNNER_TEMP/comment.md" || true