@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jesdi/skills",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "jesdi's agent skills (content package consumed by @jesdi/skills-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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.
@@ -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
- print(render(rank_issues(issues)))
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
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-23T11:12:36.101Z",
3
+ "generatedAt": "2026-07-23T11:21:38.913Z",
4
4
  "skills": [
5
5
  {
6
6
  "name": "backlog",
7
- "version": "0.1.1",
8
- "hash": "sha256-2a594bc97cd045b92b5771f6c462b861b8adcfaee0135c59820af092bdf02a2f",
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
  {