Skip to content

Commit 0ba448c

Browse files
Merge pull request #4680 from springfall2008/feat/issue-triage-skill
Add automated first-pass issue triage skill and daemon
2 parents 5bf2fe1 + 9bec4b9 commit 0ba448c

3 files changed

Lines changed: 227 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
name: issue-triage
3+
description: Investigate a newly opened batpred GitHub issue against current main, classify it, assign labels and priority, and post a first-pass triage comment.
4+
allowed-tools: You can view and edit files in this repo, run local tests, access the issue in github with 'gh' and use git commands to check history etc. Do not go outside this sandbox or push any changes back to git.
5+
---
6+
7+
# Issue Triage
8+
9+
You are triaging one GitHub issue on `springfall2008/batpred`. The issue number is given as an argument to this skill invocation. The working directory is a dedicated local clone already synced to the current `main` branch — investigate using it directly.
10+
11+
## 1. Read the issue
12+
13+
Fetch it with `gh issue view <number> --json title,body,labels,comments`. Note any labels already applied — never remove a label a human added.
14+
15+
## 2. Fetch attachments
16+
17+
If the body links a log file or `predbat_debug.yaml`, fetch and read them. Use the log for error/traceback context and the debug yaml for the reporter's actual configuration.
18+
19+
## 3. Classify the type
20+
21+
Apply exactly one of: `bug`, `question`, `configuration` (user error/misconfiguration), `enhancement` (feature request). If genuinely ambiguous, apply `unclear` instead of guessing.
22+
23+
## 4. Check for duplicates
24+
25+
Search existing issues (`gh issue list --search ...`, both open and closed) for the same symptom.
26+
27+
- High confidence match: label `duplicate`, comment linking the original issue, and close this one.
28+
- Low confidence: mention "possibly related to #N" in your triage comment; don't close.
29+
30+
## 5. Investigate against current main
31+
32+
- This clone copy should be sync'ed to main, but you should sync to latest and you can discard any local changes left over from previous runs.
33+
- Read the relevant source area for the reported symptom (e.g. `apps/predbat/fetch.py` for rate issues, `apps/predbat/inverter.py` for a named inverter).
34+
- Check `git log` / `git blame` on that area for recent related changes — the issue may already be fixed on main since the version the reporter is using.
35+
- If the issue clearly maps to an existing test module (`apps/predbat/tests/test_<feature>.py`, listed in `TEST_REGISTRY` in `unit_test.py`), run just that test: `cd coverage && ./run_all --test <name>`. Skip this step if there's no clean mapping — never run the full suite.
36+
- Form a root-cause hypothesis with a `file:line` pointer if the investigation supports one. It's fine to say the cause needs maintainer review if it doesn't.
37+
- You may edit files locally to test a hypothesis (e.g. a temporary debug print, a tweaked test fixture) — this clone is hard-reset before the next run, so nothing here persists. Never `git commit` or `git push`.
38+
39+
## 6. Apply component labels
40+
41+
Only when the issue text clearly names a specific inverter or integration (e.g. `solis`, `Huawei`, `solaredge`, `Octopus`, `fox`), apply the matching existing label. Don't guess.
42+
43+
## 7. Check for missing information
44+
45+
For bug reports specifically, verify these four things (from the bug report template) are actually present: Predbat version, inverter/environment details, log file, `predbat_debug.yaml`. If something missing is actually needed for your investigation, apply `waiting_for_user` and ask only for what's missing — don't ask for things already provided.
46+
47+
## 8. Set priority
48+
49+
Apply exactly one of:
50+
51+
- `priority_high` — impacts many users, no workaround
52+
- `priority_medium` — impacts many users, workaround exists
53+
- `priority_low` — limited impact or an easy workaround
54+
55+
Skip priority for pure questions and feature requests.
56+
57+
## 9. Post one comment
58+
59+
Check existing comments first — if one from you is already there, stop; don't post again.
60+
61+
Post exactly one comment via `gh issue comment <number> --body "..."`, opening with a line disclosing this is an automated first-pass triage (a maintainer will review before any action is taken), followed by: classification, priority (if set), what you investigated and found (including test result if you ran one), a root-cause pointer if you have one, and any information request.
62+
63+
## Guardrails
64+
65+
- Analysis only: no commits, no pushes, no PRs, no code changes that leave this clone.
66+
- Never remove a label a human applied.
67+
- Never close an issue except a confident duplicate.

‎.cspell/custom-dictionary-workspace.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ njpwerner
373373
nobat
374374
nocharge
375375
nodischarge
376+
nohup
376377
noopener
377378
nord
378379
nordpool

‎tools/triage_daemon.py‎

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env python3
2+
"""Polls springfall2008/batpred for new GitHub issues and triages each one
3+
with Claude Code, using a dedicated local clone kept in sync with main.
4+
5+
Setup (one-time, on whichever always-on machine will run this):
6+
7+
1. Update Claude Code first: `claude update`. The --max-budget-usd cap
8+
below only enforces spend on v2.1.217+; older versions accept the
9+
flag but won't actually cap anything.
10+
11+
2. Clone a copy of the repo dedicated to this daemon - do NOT point it
12+
at a machine's normal working copy. Each run does a hard reset to
13+
origin/main before invoking Claude, which would blow away any
14+
in-progress work in a real dev checkout.
15+
16+
mkdir -p ~/predbat-triage-bot
17+
cd ~/predbat-triage-bot
18+
git clone https://github.com/springfall2008/batpred
19+
20+
3. Set up the test venv once, so the skill can run targeted tests:
21+
22+
cd ~/predbat-triage-bot/batpred/coverage
23+
source setup.csh
24+
25+
4. Seed the state file with the CURRENT highest open issue number, so
26+
the first run doesn't triage the entire existing backlog:
27+
28+
gh issue list --repo springfall2008/batpred --state open \
29+
--json number --limit 200 \
30+
| python3 -c "import json,sys; print(max(i['number'] for i in json.load(sys.stdin)))"
31+
32+
echo '{"last_processed": <number from above>}' > ~/predbat-triage-bot/state.json
33+
34+
5. Run it. It polls every 5 minutes and keeps running until stopped
35+
(Ctrl-C, or run it under nohup/tmux/a LaunchAgent to survive a
36+
closed terminal):
37+
38+
python3 tools/triage_daemon.py
39+
40+
What it does per new issue: syncs the dedicated clone to origin/main,
41+
then runs `claude -p "/issue-triage <number>"` (the skill at
42+
.claude/skills/issue-triage/SKILL.md) with a permission mode that
43+
denies anything not explicitly listed below - git/gh read commands,
44+
running one targeted test, editing files only inside this dedicated
45+
clone, and fetching issue attachments. All MCP tools are explicitly
46+
denied (--disallowedTools "mcp__*"), and --max-turns/--max-budget-usd
47+
cap a single invocation from running away.
48+
"""
49+
50+
import json
51+
import subprocess
52+
import time
53+
from pathlib import Path
54+
55+
REPO = "springfall2008/batpred"
56+
BASE_DIR = Path.home() / "predbat-triage-bot"
57+
CLONE_DIR = BASE_DIR / "batpred"
58+
STATE_FILE = BASE_DIR / "state.json"
59+
POLL_SECONDS = 300
60+
61+
EDIT_SCOPE = f"//{CLONE_DIR.relative_to('/')}/**"
62+
ALLOWED_TOOLS = ",".join(
63+
[
64+
"Bash(gh *)",
65+
"Bash(git log*)",
66+
"Bash(git diff*)",
67+
"Bash(git show*)",
68+
"Bash(git blame*)",
69+
"Bash(git grep*)",
70+
"Bash(cd *)",
71+
"Bash(./run_all*)",
72+
f"Edit({EDIT_SCOPE})",
73+
"WebFetch",
74+
"Read",
75+
"Grep",
76+
"Glob",
77+
]
78+
)
79+
80+
81+
def load_state():
82+
try:
83+
if STATE_FILE.exists():
84+
return json.loads(STATE_FILE.read_text())
85+
except json.JSONDecodeError:
86+
return {"last_processed": 0}
87+
return {"last_processed": 0}
88+
89+
90+
def save_state(state):
91+
BASE_DIR.mkdir(parents=True, exist_ok=True)
92+
tmp_path = STATE_FILE.with_suffix(STATE_FILE.suffix + ".tmp")
93+
tmp_path.write_text(json.dumps(state))
94+
tmp_path.replace(STATE_FILE)
95+
96+
97+
def fetch_new_issues(since_number):
98+
result = subprocess.run(
99+
["gh", "issue", "list", "--repo", REPO, "--state", "open", "--json", "number,createdAt", "--limit", "100"],
100+
capture_output=True,
101+
text=True,
102+
check=True,
103+
)
104+
issues = json.loads(result.stdout)
105+
new = [i for i in issues if i["number"] > since_number]
106+
return sorted(new, key=lambda i: i["number"])
107+
108+
109+
def sync_repo():
110+
subprocess.run(["git", "-C", str(CLONE_DIR), "fetch", "origin", "main"], check=True)
111+
subprocess.run(["git", "-C", str(CLONE_DIR), "reset", "--hard", "origin/main"], check=True)
112+
113+
114+
def triage(issue_number):
115+
cmd = [
116+
"claude",
117+
"-p",
118+
f"/issue-triage {issue_number}",
119+
"--permission-mode",
120+
"dontAsk",
121+
"--allowedTools",
122+
ALLOWED_TOOLS,
123+
"--disallowedTools",
124+
"mcp__*",
125+
"--max-turns",
126+
"40",
127+
# Client-side token-usage estimate, not a real spend cap under subscription
128+
# auth (see agent-sdk/cost-tracking) - just a circuit-breaker against a
129+
# runaway invocation, sized generously since one issue can need several
130+
# file reads plus a test run.
131+
"--max-budget-usd",
132+
"10.00",
133+
]
134+
print(f"[triage] issue #{issue_number}: starting", flush=True)
135+
result = subprocess.run(cmd, cwd=str(CLONE_DIR))
136+
if result.returncode != 0:
137+
raise subprocess.CalledProcessError(result.returncode, cmd)
138+
print(f"[triage] issue #{issue_number}: exited {result.returncode}", flush=True)
139+
140+
141+
def main():
142+
if not CLONE_DIR.exists():
143+
raise SystemExit(f"Expected a git clone at {CLONE_DIR} - see setup steps before running this daemon.")
144+
145+
state = load_state()
146+
while True:
147+
try:
148+
for issue in fetch_new_issues(state["last_processed"]):
149+
sync_repo()
150+
triage(issue["number"])
151+
state["last_processed"] = issue["number"]
152+
save_state(state)
153+
except subprocess.CalledProcessError as exc:
154+
print(f"[triage] error: {exc}", flush=True)
155+
time.sleep(POLL_SECONDS)
156+
157+
158+
if __name__ == "__main__":
159+
main()

0 commit comments

Comments
 (0)