@jesdi/skills 0.0.6 → 0.0.7

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.6",
3
+ "version": "0.0.7",
4
4
  "description": "jesdi's agent skills (content package consumed by @jesdi/skills-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,7 +52,7 @@ dir.
52
52
  1. Ensure the `project` scope: `gh auth status`; if Projects calls 403, run
53
53
  `gh auth refresh -s project`.
54
54
  2. Follow `references/graphql.md` → **Setup**: create the Project, link the repo,
55
- add the four fields, create the `inbox` label. On a brand-new board the `Area`
55
+ add the six fields, create the `inbox` label. On a brand-new board the `Area`
56
56
  single-select is created with the generic default options
57
57
  `feature,bug,infra,docs,research`. If any step reports "already exists", treat
58
58
  it as done and continue. Projects that want different Areas edit the option set
@@ -137,6 +137,12 @@ Reuse the **dedup discipline** from the `file-bug-issue` skill, but take the
137
137
  3. For a promoted issue, gather `Impact` (1–5), `Effort` (1–5), `Area` (**pick one
138
138
  from the options present in `.backlog/project-meta.json`** → `fields.Area.options`),
139
139
  and optional blockers.
140
+
141
+ - **Boost** (number, optional, default 0): manual priority override. Ranking is
142
+ band-dominant — any issue with higher Boost outranks every issue with lower
143
+ Boost regardless of score; score orders issues within a band. Negative values
144
+ sink an issue below the unboosted pack. Dispatchers set 99 for "work on this
145
+ next". `--json` rows carry `"boost"`.
140
146
  4. Apply, in order, the `references/graphql.md` → Triage commands:
141
147
  add to project → set Impact/Effort (`--number`) → set
142
148
  `Score` = round(Impact ÷ Effort, 1) (`--number`) → set Area +
@@ -155,7 +161,7 @@ python3 .claude/skills/backlog/rank.py # human-readable table
155
161
  python3 .claude/skills/backlog/rank.py --json # machine-readable rows for
156
162
  # dispatchers: number, title,
157
163
  # url, status, labels, blocked,
158
- # score
164
+ # score, boost
159
165
  ```
160
166
 
161
167
  (Use whichever agent-skills path the store symlinked the skill into — e.g.
@@ -165,10 +171,11 @@ or below the repo root so `rank.py` can walk up to `.backlog/`.)
165
171
  `rank.py` pulls Project items + issue bodies via `gh`, parses `Blocked by:`
166
172
  edges, drops closed/`Done` blockers, and prints:
167
173
 
168
- - **Available** issues ranked by Impact÷Effort (unscored sort last, flagged
169
- `[needs triage]`).
174
+ - **Available** issues ranked boost-band first, then Impact÷Effort within a band
175
+ (unscored sort last within a band, flagged `[needs triage]`).
170
176
  - **Blocked** issues, unranked, annotated with what they wait on.
171
177
 
172
- Present the table as-is. If the user asks "why isn't #N at the top", point at its
173
- score or its blockers from the output. This in-memory ranking is the reason the
178
+ Present the table as-is. If the user asks "why isn't #N at the top", check first
179
+ whether another issue has a higher Boost band (shown as `↑n`), then its score, then
180
+ its blockers from the output. This in-memory ranking is the reason the
174
181
  skill exists over a bare `gh issue list`.
@@ -11,6 +11,7 @@
11
11
  "Impact": {"id": "PVTF_REPLACE"},
12
12
  "Effort": {"id": "PVTF_REPLACE"},
13
13
  "Score": {"id": "PVTF_REPLACE"},
14
+ "Boost": {"id": "PVTF_REPLACE"},
14
15
  "Area": {
15
16
  "id": "PVTSSF_REPLACE",
16
17
  "options": {"feature": "...", "bug": "...", "infra": "...", "docs": "...", "research": "..."}
@@ -66,6 +66,7 @@ def merge_sources(project_items, issue_rows):
66
66
  "effort": _as_int(item.get("effort")),
67
67
  "area": item.get("area"),
68
68
  "labels": [l["name"] for l in row.get("labels") or []],
69
+ "boost": (lambda v: 0 if v is None else v)(_as_int(item.get("boost"))),
69
70
  })
70
71
  return merged
71
72
 
@@ -100,7 +101,8 @@ def score(issue):
100
101
  def _sort_key(issue):
101
102
  value = score(issue)
102
103
  return (
103
- 0 if value is not None else 1, # scored issues first
104
+ -issue.get("boost", 0), # band first: higher boost above everything
105
+ 0 if value is not None else 1, # scored issues first within a band
104
106
  -(value or 0.0), # higher score first
105
107
  -(issue.get("impact") or 0), # tiebreak: higher impact
106
108
  issue["number"], # stable, deterministic
@@ -146,6 +148,7 @@ def to_json_rows(result):
146
148
  "labels": issue.get("labels", []),
147
149
  "blocked": blocked,
148
150
  "score": score(issue),
151
+ "boost": issue.get("boost", 0),
149
152
  })
150
153
  return rows
151
154
 
@@ -165,6 +168,11 @@ def render(result):
165
168
  value = score(issue)
166
169
  score_str = f"{value:.2f}" if value is not None else " — "
167
170
  flag = "" if value is not None else " [needs triage]"
171
+ boost = issue.get("boost", 0)
172
+ if boost > 0:
173
+ flag += f" ↑{boost}"
174
+ elif boost < 0:
175
+ flag += f" ↓{-boost}"
168
176
  label = f"#{issue['number']} {issue['title']}"[:38].ljust(38)
169
177
  area = (issue.get("area") or "-")
170
178
  lines.append(f"{idx:>4} {score_str:>5} {label} {area:<10}{flag}")
@@ -26,6 +26,7 @@ gh project field-create <projectNumber> --owner <owner> --name "Status" \
26
26
  gh project field-create <projectNumber> --owner <owner> --name "Impact" --data-type NUMBER
27
27
  gh project field-create <projectNumber> --owner <owner> --name "Effort" --data-type NUMBER
28
28
  gh project field-create <projectNumber> --owner <owner> --name "Score" --data-type NUMBER
29
+ gh project field-create <projectNumber> --owner <owner> --name "Boost" --data-type NUMBER
29
30
  gh project field-create <projectNumber> --owner <owner> --name "Area" \
30
31
  --data-type SINGLE_SELECT \
31
32
  --single-select-options "feature,bug,infra,docs,research"
@@ -65,6 +66,9 @@ gh project item-edit --project-id <projectId> --id <itemId> \
65
66
  --field-id <fields.Effort.id> --number <1-5>
66
67
  gh project item-edit --project-id <projectId> --id <itemId> \
67
68
  --field-id <fields.Score.id> --number <round(impact/effort, 1)>
69
+ # Optional: manual priority override (default 0; positive boosts above unboosted, negative sinks below)
70
+ gh project item-edit --project-id <projectId> --id <itemId> \
71
+ --field-id <fields.Boost.id> --number <n>
68
72
 
69
73
  # Set single-selects (use the option id from .backlog/project-meta.json)
70
74
  gh project item-edit --project-id <projectId> --id <itemId> \
@@ -302,7 +302,7 @@ def test_main_json_emits_machine_readable_rows(monkeypatch, capsys):
302
302
  rows = jsonlib.loads(capsys.readouterr().out)
303
303
  assert rows == [{"number": 5, "title": "Task", "url": "u/5",
304
304
  "status": "Ready", "labels": ["auto"],
305
- "blocked": False, "score": 2.0}]
305
+ "blocked": False, "score": 2.0, "boost": 0}]
306
306
 
307
307
 
308
308
  def test_project_env_absent_without_token(monkeypatch):
@@ -343,3 +343,72 @@ def test_find_project_meta_errors_clearly_when_absent(tmp_path, monkeypatch):
343
343
  rank.find_project_meta()
344
344
  assert ".backlog/project-meta.json" in str(exc.value)
345
345
  assert "backlog setup" in str(exc.value)
346
+
347
+
348
+ def test_merge_sources_parses_boost_and_defaults_zero():
349
+ out = rank.merge_sources(
350
+ [{"content": {"number": 7, "title": "T", "url": "u/7"}, "boost": 2},
351
+ {"content": {"number": 8, "title": "U", "url": "u/8"}, "boost": ""},
352
+ {"content": {"number": 9, "title": "V", "url": "u/9"}},
353
+ {"content": {"number": 10, "title": "W", "url": "u/10"}, "boost": -3}],
354
+ [])
355
+ assert [i["boost"] for i in out] == [2, 0, 0, -3]
356
+
357
+
358
+ def test_boost_band_dominates_score():
359
+ lo = _scored(1, 5, 1) # score 5.0, unboosted
360
+ hi = _scored(2, 1, 2) # score 0.5, boosted
361
+ hi["boost"] = 1
362
+ result = rank.rank_issues([lo, hi])
363
+ assert [i["number"] for i in result["available"]] == [2, 1]
364
+
365
+
366
+ def test_score_orders_within_boost_band():
367
+ a = _scored(1, 2, 2) # score 1.0
368
+ b = _scored(2, 5, 1) # score 5.0
369
+ a["boost"] = b["boost"] = 1
370
+ result = rank.rank_issues([a, b])
371
+ assert [i["number"] for i in result["available"]] == [2, 1]
372
+
373
+
374
+ def test_negative_boost_sinks_below_unboosted():
375
+ demoted = _scored(1, 9, 1) # score 9.0 but demoted
376
+ demoted["boost"] = -1
377
+ plain = _scored(2, 1, 1) # score 1.0
378
+ result = rank.rank_issues([demoted, plain])
379
+ assert [i["number"] for i in result["available"]] == [2, 1]
380
+
381
+
382
+ def test_unscored_boosted_enters_band_after_scored_in_band():
383
+ scored_boosted = _scored(1, 2, 1)
384
+ scored_boosted["boost"] = 1
385
+ unscored_boosted = _scored(2, None, None)
386
+ unscored_boosted["boost"] = 1
387
+ scored_plain = _scored(3, 5, 1)
388
+ result = rank.rank_issues([scored_plain, unscored_boosted, scored_boosted])
389
+ assert [i["number"] for i in result["available"]] == [1, 2, 3]
390
+
391
+
392
+ def test_json_rows_include_boost():
393
+ a = _scored(1, 2, 1)
394
+ a["boost"] = 2
395
+ a["labels"] = ["auto"]
396
+ b = _scored(2, 5, 1)
397
+ b["labels"] = ["auto"]
398
+ rows = rank.to_json_rows(rank.rank_issues([a, b]))
399
+ assert [(r["number"], r["boost"]) for r in rows] == [(1, 2), (2, 0)]
400
+
401
+
402
+ def test_render_marks_boost_up_and_down():
403
+ up = _scored(1, 2, 1)
404
+ up["boost"] = 2
405
+ down = _scored(2, 5, 1)
406
+ down["boost"] = -1
407
+ plain = _scored(3, 1, 1)
408
+ text = rank.render(rank.rank_issues([up, down, plain]))
409
+ line_for_up = next(l for l in text.splitlines() if "#1 i1" in l)
410
+ assert "↑2" in line_for_up
411
+ line_for_down = next(l for l in text.splitlines() if "#2 i2" in l)
412
+ assert "↓1" in line_for_down
413
+ line_for_plain = next(l for l in text.splitlines() if "#3 i3" in l)
414
+ assert "↑" not in line_for_plain and "↓" not in line_for_plain
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-23T15:49:56.550Z",
3
+ "generatedAt": "2026-07-24T16:48:47.031Z",
4
4
  "skills": [
5
5
  {
6
6
  "name": "backlog",
7
- "version": "0.1.3",
8
- "hash": "sha256-afe5ef66a86a4bbb1ce654e9ec861be80873e36e1cb45fbe132564a25dc26c1d",
7
+ "version": "0.1.4",
8
+ "hash": "sha256-c5c2ff8482d24eb782b0d5799b1e490733340f604196f42e8051d731bb00dd09",
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
  {