@jesdi/skills 0.0.4 → 0.0.5
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.
- package/package.json +1 -1
- package/skills/backlog/SKILL.md +5 -1
- package/skills/backlog/rank.py +32 -4
- package/skills/backlog/test_rank.py +57 -0
- package/skills-manifest.json +3 -3
package/package.json
CHANGED
package/skills/backlog/SKILL.md
CHANGED
|
@@ -151,7 +151,11 @@ Runs the pure engine; makes no writes. Run **from the project root** (so the cwd
|
|
|
151
151
|
walk-up finds `.backlog/project-meta.json`); do **not** `cd` into the skill dir:
|
|
152
152
|
|
|
153
153
|
```bash
|
|
154
|
-
python3 .claude/skills/backlog/rank.py
|
|
154
|
+
python3 .claude/skills/backlog/rank.py # human-readable table
|
|
155
|
+
python3 .claude/skills/backlog/rank.py --json # machine-readable rows for
|
|
156
|
+
# dispatchers: number, title,
|
|
157
|
+
# url, status, labels, blocked,
|
|
158
|
+
# score
|
|
155
159
|
```
|
|
156
160
|
|
|
157
161
|
(Use whichever agent-skills path the store symlinked the skill into — e.g.
|
package/skills/backlog/rank.py
CHANGED
|
@@ -48,6 +48,7 @@ def merge_sources(project_items, issue_rows):
|
|
|
48
48
|
"impact": _as_int(item.get("impact")),
|
|
49
49
|
"effort": _as_int(item.get("effort")),
|
|
50
50
|
"area": item.get("area"),
|
|
51
|
+
"labels": [l["name"] for l in row.get("labels") or []],
|
|
51
52
|
})
|
|
52
53
|
return merged
|
|
53
54
|
|
|
@@ -111,6 +112,27 @@ def rank_issues(issues):
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
|
|
115
|
+
def to_json_rows(result):
|
|
116
|
+
"""Flat machine-readable rows for `--json` consumers (e.g. dispatchers):
|
|
117
|
+
available issues in rank order, then blocked, then in-progress."""
|
|
118
|
+
rows = []
|
|
119
|
+
for issue, blocked in (
|
|
120
|
+
[(i, False) for i in result["available"]]
|
|
121
|
+
+ [(i, True) for i in result["blocked"]]
|
|
122
|
+
+ [(i, False) for i in result["in_progress"]]
|
|
123
|
+
):
|
|
124
|
+
rows.append({
|
|
125
|
+
"number": issue["number"],
|
|
126
|
+
"title": issue["title"],
|
|
127
|
+
"url": issue["url"],
|
|
128
|
+
"status": issue.get("status"),
|
|
129
|
+
"labels": issue.get("labels", []),
|
|
130
|
+
"blocked": blocked,
|
|
131
|
+
"score": score(issue),
|
|
132
|
+
})
|
|
133
|
+
return rows
|
|
134
|
+
|
|
135
|
+
|
|
114
136
|
def render(result):
|
|
115
137
|
lines = []
|
|
116
138
|
if result["in_progress"]:
|
|
@@ -153,7 +175,7 @@ def _project_env():
|
|
|
153
175
|
return {**os.environ, "GH_TOKEN": token} if token else None
|
|
154
176
|
|
|
155
177
|
|
|
156
|
-
def main(owner, project_number, repo):
|
|
178
|
+
def main(owner, project_number, repo, as_json=False):
|
|
157
179
|
payload = _gh_json([
|
|
158
180
|
"gh", "project", "item-list", str(project_number),
|
|
159
181
|
"--owner", owner, "--format", "json", "--limit", "200",
|
|
@@ -161,10 +183,14 @@ def main(owner, project_number, repo):
|
|
|
161
183
|
project_items = payload.get("items", [])
|
|
162
184
|
issue_rows = _gh_json([
|
|
163
185
|
"gh", "issue", "list", "--repo", repo, "--state", "all",
|
|
164
|
-
"--limit", "500", "--json", "number,body,state",
|
|
186
|
+
"--limit", "500", "--json", "number,body,state,labels",
|
|
165
187
|
])
|
|
166
188
|
issues = merge_sources(project_items, issue_rows)
|
|
167
|
-
|
|
189
|
+
result = rank_issues(issues)
|
|
190
|
+
if as_json:
|
|
191
|
+
print(json.dumps(to_json_rows(result)))
|
|
192
|
+
else:
|
|
193
|
+
print(render(result))
|
|
168
194
|
|
|
169
195
|
|
|
170
196
|
def find_project_meta(start=None):
|
|
@@ -189,7 +215,9 @@ def find_project_meta(start=None):
|
|
|
189
215
|
|
|
190
216
|
|
|
191
217
|
if __name__ == "__main__":
|
|
218
|
+
import sys
|
|
192
219
|
config_path = find_project_meta()
|
|
193
220
|
with open(config_path) as handle:
|
|
194
221
|
meta = json.load(handle)
|
|
195
|
-
main(meta["owner"], meta["projectNumber"], meta["repo"]
|
|
222
|
+
main(meta["owner"], meta["projectNumber"], meta["repo"],
|
|
223
|
+
as_json="--json" in sys.argv[1:])
|
|
@@ -206,6 +206,63 @@ def test_find_project_meta_nearest_ancestor_wins(tmp_path, monkeypatch):
|
|
|
206
206
|
assert rank.find_project_meta() == str(inner_meta)
|
|
207
207
|
|
|
208
208
|
|
|
209
|
+
def test_merge_sources_carries_label_names():
|
|
210
|
+
out = rank.merge_sources(
|
|
211
|
+
[{"content": {"number": 7, "title": "T", "url": "u/7"}}],
|
|
212
|
+
[{"number": 7, "body": "", "state": "open",
|
|
213
|
+
"labels": [{"name": "auto"}, {"name": "bug"}]}],
|
|
214
|
+
)
|
|
215
|
+
assert out[0]["labels"] == ["auto", "bug"]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_merge_sources_defaults_labels_empty():
|
|
219
|
+
out = rank.merge_sources(
|
|
220
|
+
[{"content": {"number": 7, "title": "T", "url": "u/7"}}], [])
|
|
221
|
+
assert out[0]["labels"] == []
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_json_rows_flags_blocked_and_keeps_rank_order():
|
|
225
|
+
issues = [
|
|
226
|
+
_scored(1, 2, 2), # score 1.0
|
|
227
|
+
_scored(2, 5, 1), # score 5.0 -> first
|
|
228
|
+
_scored(3, 9, 1, body="Blocked by: #2"), # blocked
|
|
229
|
+
_scored(4, 4, 1, status="In progress"), # claimed
|
|
230
|
+
]
|
|
231
|
+
for i in issues:
|
|
232
|
+
i["labels"] = ["auto"]
|
|
233
|
+
rows = rank.to_json_rows(rank.rank_issues(issues))
|
|
234
|
+
assert [(r["number"], r["blocked"]) for r in rows] == [
|
|
235
|
+
(2, False), (1, False), (3, True), (4, False)]
|
|
236
|
+
row = rows[0]
|
|
237
|
+
assert row["title"] == "i2" and row["status"] == "Ready"
|
|
238
|
+
assert row["labels"] == ["auto"]
|
|
239
|
+
assert "url" in row
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_main_json_emits_machine_readable_rows(monkeypatch, capsys):
|
|
243
|
+
import json as jsonlib
|
|
244
|
+
monkeypatch.delenv("GH_PROJECT_TOKEN", raising=False)
|
|
245
|
+
def fake_run(args, capture_output, text, check, env=None):
|
|
246
|
+
class R:
|
|
247
|
+
if args[1] == "project":
|
|
248
|
+
stdout = jsonlib.dumps({"items": [
|
|
249
|
+
{"content": {"number": 5, "title": "Task", "url": "u/5"},
|
|
250
|
+
"status": "Ready", "impact": 4, "effort": 2}]})
|
|
251
|
+
else:
|
|
252
|
+
assert "labels" in args[args.index("--json") + 1]
|
|
253
|
+
stdout = jsonlib.dumps([
|
|
254
|
+
{"number": 5, "body": "", "state": "open",
|
|
255
|
+
"labels": [{"name": "auto"}]}])
|
|
256
|
+
return R()
|
|
257
|
+
|
|
258
|
+
monkeypatch.setattr(rank.subprocess, "run", fake_run)
|
|
259
|
+
rank.main("acme", 1, "acme/private-repo", as_json=True)
|
|
260
|
+
rows = jsonlib.loads(capsys.readouterr().out)
|
|
261
|
+
assert rows == [{"number": 5, "title": "Task", "url": "u/5",
|
|
262
|
+
"status": "Ready", "labels": ["auto"],
|
|
263
|
+
"blocked": False, "score": 2.0}]
|
|
264
|
+
|
|
265
|
+
|
|
209
266
|
def test_project_env_absent_without_token(monkeypatch):
|
|
210
267
|
monkeypatch.delenv("GH_PROJECT_TOKEN", raising=False)
|
|
211
268
|
assert rank._project_env() is None
|
package/skills-manifest.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-07-23T11:
|
|
3
|
+
"generatedAt": "2026-07-23T11:21:38.913Z",
|
|
4
4
|
"skills": [
|
|
5
5
|
{
|
|
6
6
|
"name": "backlog",
|
|
7
|
-
"version": "0.1.
|
|
8
|
-
"hash": "sha256-
|
|
7
|
+
"version": "0.1.2",
|
|
8
|
+
"hash": "sha256-40e91ee1f155c778ee596fd4f459cacbdcf8221bf95b0f9f2e45652207aaa275",
|
|
9
9
|
"description": "Manage a GitHub-Issues-backed backlog and a Projects-v2 work graph via five verbs. Use when the user wants to CAPTURE an idea/research thread (\"add to backlog\", \"capture this\", \"note this idea\", \"log a task\"), TRIAGE the inbox (\"triage the backlog\", \"score the inbox\"), see what to work on NEXT (\"what's next\", \"what should I work on\", \"ranked backlog\", \"next actionable task\"), LAND a finished item (\"mark done\", \"close out #N\"), or SETUP the board the first time. Ranks open issues by impact÷effort, respecting `Blocked by:` edges, so only actionable work surfaces."
|
|
10
10
|
},
|
|
11
11
|
{
|