canvasxpress-cli 71.1.0 → 71.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env python3
2
+ """Triage a freshly opened GitHub issue with Claude.
3
+
4
+ Reads the issue from the environment (populated by the workflow), asks Claude to
5
+ decide whether it is a well-defined, meritorious issue, then acts via the `gh`
6
+ CLI:
7
+
8
+ * needs-info -> post a comment asking for the specific missing detail and add
9
+ the `needs-more-info` label.
10
+ * valid -> add the `bug` or `enhancement` label and assign @neuhausi.
11
+
12
+ The model is instructed to return a single strict-JSON object so we never parse
13
+ prose. Any failure (missing key, bad JSON, API error) is non-fatal: we leave the
14
+ issue untouched and exit 0 so a transient hiccup never blocks a contributor.
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import subprocess
20
+ import sys
21
+
22
+ import anthropic
23
+
24
+ # ---- Decision contract ------------------------------------------------------
25
+ # Claude must return exactly this shape. Keeping it tiny makes it reliable.
26
+ SYSTEM_PROMPT = """\
27
+ You triage incoming GitHub issues for an open-source software project. Judge ONLY
28
+ the issue as written against the repository context. Decide:
29
+
30
+ 1. Is it well-defined? A well-defined issue states what happened or what is wanted
31
+ clearly enough to act on. A BUG report needs: what was done, what was expected,
32
+ and what happened instead (ideally a reproducer, code, version, or error text).
33
+ A FEATURE request needs: the capability wanted and why / the use case.
34
+ 2. Does it have merit? It is on-topic for THIS repository and is not spam, a
35
+ duplicate-looking one-liner, a support question better asked elsewhere, or empty.
36
+
37
+ Return a SINGLE JSON object, no markdown fence, with these fields:
38
+ "decision": "valid" | "needs_info"
39
+ "type": "bug" | "feature" | null // the kind, when decision is "valid"; else null
40
+ "reasons": [string, ...] // brief, why you decided this
41
+ "comment": string // the GitHub comment body to post (see below)
42
+
43
+ Rules for "comment":
44
+ * If decision is "needs_info": write a friendly, specific request listing exactly
45
+ what the reporter should add (e.g. a minimal reproducer, the exact input/prompt,
46
+ expected vs actual output, versions, error text). Reference what IS present so it
47
+ does not feel generic. Markdown is fine.
48
+ * If decision is "valid": write a short acknowledgement confirming it was triaged
49
+ as a bug or feature and assigned for review. One or two sentences.
50
+ Never invent facts about the project. Be concise and kind.
51
+ """
52
+
53
+
54
+ def getenv(name, default=""):
55
+ return os.environ.get(name, default) or default
56
+
57
+
58
+ def run_gh(args):
59
+ """Run a `gh` command, returning (ok, output). Never raises."""
60
+ try:
61
+ result = subprocess.run(
62
+ ["gh"] + args,
63
+ capture_output=True,
64
+ text=True,
65
+ check=False,
66
+ )
67
+ if result.returncode != 0:
68
+ sys.stderr.write("gh %s failed: %s\n" % (" ".join(args), result.stderr.strip()))
69
+ return False, result.stderr
70
+ return True, result.stdout
71
+ except FileNotFoundError:
72
+ sys.stderr.write("gh CLI not found on runner\n")
73
+ return False, ""
74
+
75
+
76
+ def ensure_label(repo, name, color, description):
77
+ """Create the label if it does not already exist (idempotent)."""
78
+ run_gh([
79
+ "label", "create", name,
80
+ "--repo", repo,
81
+ "--color", color,
82
+ "--description", description,
83
+ "--force", # update color/description if it exists, don't error
84
+ ])
85
+
86
+
87
+ def ask_claude(model, repo_context, title, body):
88
+ """Return the parsed decision dict, or None on any failure."""
89
+ client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env
90
+ user_content = (
91
+ "Repository context:\n%s\n\n"
92
+ "Issue title:\n%s\n\n"
93
+ "Issue body:\n%s\n"
94
+ ) % (repo_context, title, body or "(empty)")
95
+
96
+ try:
97
+ message = client.messages.create(
98
+ model=model,
99
+ max_tokens=1024,
100
+ system=SYSTEM_PROMPT,
101
+ messages=[{"role": "user", "content": user_content}],
102
+ )
103
+ except Exception as exc: # noqa: BLE001 - triage must never hard-fail
104
+ sys.stderr.write("Anthropic API error: %s\n" % exc)
105
+ return None
106
+
107
+ text = "".join(block.text for block in message.content if block.type == "text").strip()
108
+ # Tolerate an accidental ```json fence.
109
+ if text.startswith("```"):
110
+ text = text.strip("`")
111
+ text = text[text.find("{"): text.rfind("}") + 1]
112
+
113
+ try:
114
+ decision = json.loads(text)
115
+ except json.JSONDecodeError:
116
+ sys.stderr.write("Could not parse model JSON:\n%s\n" % text)
117
+ return None
118
+
119
+ if decision.get("decision") not in ("valid", "needs_info"):
120
+ sys.stderr.write("Unexpected decision value: %r\n" % decision.get("decision"))
121
+ return None
122
+ return decision
123
+
124
+
125
+ def main():
126
+ repo = getenv("GITHUB_REPOSITORY")
127
+ number = getenv("ISSUE_NUMBER")
128
+ title = getenv("ISSUE_TITLE")
129
+ body = getenv("ISSUE_BODY")
130
+ assignee = getenv("ASSIGNEE", "neuhausi")
131
+ model = getenv("TRIAGE_MODEL", "claude-sonnet-5-5")
132
+ repo_context = getenv("REPO_CONTEXT", "A CanvasXpress companion repository.")
133
+
134
+ if not repo or not number:
135
+ sys.stderr.write("Missing GITHUB_REPOSITORY or ISSUE_NUMBER; nothing to do.\n")
136
+ return 0
137
+
138
+ decision = ask_claude(model, repo_context, title, body)
139
+ if decision is None:
140
+ # Soft-fail: leave the issue for a human rather than guessing.
141
+ return 0
142
+
143
+ comment = decision.get("comment") or ""
144
+ print("Decision: %s (type=%s)" % (decision.get("decision"), decision.get("type")))
145
+ for reason in decision.get("reasons", []):
146
+ print(" - %s" % reason)
147
+
148
+ # Make sure the labels we use exist, with friendly colors.
149
+ ensure_label(repo, "needs-more-info", "d4c5f9", "Awaiting more detail from the reporter")
150
+ ensure_label(repo, "bug", "d73a4a", "Something isn't working")
151
+ ensure_label(repo, "enhancement", "a2eeef", "New feature or request")
152
+
153
+ if decision["decision"] == "needs_info":
154
+ if comment:
155
+ run_gh(["issue", "comment", number, "--repo", repo, "--body", comment])
156
+ run_gh(["issue", "edit", number, "--repo", repo, "--add-label", "needs-more-info"])
157
+ return 0
158
+
159
+ # decision == "valid"
160
+ label = "bug" if decision.get("type") == "bug" else "enhancement"
161
+ run_gh(["issue", "edit", number, "--repo", repo, "--add-label", label])
162
+ run_gh(["issue", "edit", number, "--repo", repo, "--add-assignee", assignee])
163
+ if comment:
164
+ run_gh(["issue", "comment", number, "--repo", repo, "--body", comment])
165
+ return 0
166
+
167
+
168
+ if __name__ == "__main__":
169
+ sys.exit(main())
@@ -0,0 +1,56 @@
1
+ name: Issue triage
2
+
3
+ # Prototype: when an issue is opened (or reopened), ask Claude whether it is a
4
+ # well-defined, meritorious issue. If not, post a comment asking for the missing
5
+ # detail and label it `needs-more-info`. If it is, assign it to @neuhausi and
6
+ # label it `bug` or `enhancement`.
7
+ #
8
+ # Requires repo secret ANTHROPIC_API_KEY. GITHUB_TOKEN is provided automatically.
9
+
10
+ on:
11
+ issues:
12
+ types: [opened, reopened]
13
+
14
+ # Least-privilege: we only read repo contents and write to the issue.
15
+ permissions:
16
+ contents: read
17
+ issues: write
18
+
19
+ concurrency:
20
+ # One triage per issue at a time; a reopen supersedes an in-flight run.
21
+ group: issue-triage-${{ github.event.issue.number }}
22
+ cancel-in-progress: true
23
+
24
+ jobs:
25
+ triage:
26
+ runs-on: ubuntu-latest
27
+ # Never triage pull requests (they also surface as "issues" in some events).
28
+ if: ${{ github.event.issue.pull_request == null }}
29
+ steps:
30
+ - name: Checkout (for the triage script)
31
+ uses: actions/checkout@v4
32
+
33
+ - name: Set up Python
34
+ uses: actions/setup-python@v5
35
+ with:
36
+ python-version: "3.12"
37
+
38
+ - name: Install the Anthropic SDK
39
+ run: pip install --quiet "anthropic>=0.40"
40
+
41
+ - name: Triage the issue
42
+ env:
43
+ ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
44
+ GH_TOKEN: ${{ github.token }}
45
+ GITHUB_REPOSITORY: ${{ github.repository }}
46
+ ISSUE_NUMBER: ${{ github.event.issue.number }}
47
+ ISSUE_TITLE: ${{ github.event.issue.title }}
48
+ ISSUE_BODY: ${{ github.event.issue.body }}
49
+ ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
50
+ # Override per-repo if desired; defaults are sensible.
51
+ ASSIGNEE: neuhausi
52
+ TRIAGE_MODEL: claude-sonnet-5-5
53
+ # One-line description of THIS repo so Claude judges merit in context.
54
+ REPO_CONTEXT: >-
55
+ canvasxpress-cli is the command-line tool for generating CanvasXpress charts from data files.
56
+ run: python .github/scripts/triage_issue.py
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasxpress-cli",
3
- "version": "71.1.0",
3
+ "version": "71.3.0",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "bin": {
6
6
  "canvasxpress": "bin/canvasxpress"