diff --git a/.claude/skills/pql-board/SKILL.md b/.claude/skills/pql-board/SKILL.md index bf5aff017..4f05cbab3 100644 --- a/.claude/skills/pql-board/SKILL.md +++ b/.claude/skills/pql-board/SKILL.md @@ -63,25 +63,23 @@ Real one-click interactivity lives in the in-chat widget — render it when the user asks for "the panel", "pick-up panel", or wants to act on tickets rather than read them. -1. Pull live data (small, native projections — never embed the whole store): +1. Generate the widget code with the bundled script (template + live pql data + — do NOT hand-compose the HTML): ```bash - pql ticket list --status in_progress --fields id,type,priority,title - pql ticket list --under T-750 --unblocked --leaf --fields id,type,priority,title + .claude/skills/pql-board/support/make-panel # stdout = widget_code ``` - Panel contents: WIP + the unblocked ready work of the active phase (cap at - ~10 rows; if more, take highest priority first and say so under the panel). + Flags: `--phase T-NNN` (default T-750, the active phase epic), `--cap N` + (default 10). Contents: actionable WIP + unblocked leaf work from the + active phase AND the maintenance initiative (T-1037), sorted + status→priority→recency, overflow noted in the panel. Template: + `support/panel-template.html` (edit that file to change the panel's look; + the pick-up/detail sendPrompt contract lives there). -2. Render with `mcp__visualize__show_widget` (load its `read_me` first, - `interactive` module). Panel shape — compact bordered rows, CDS tokens: - - Each row: mono ticket id (link to the artifact deep-link `#T-NNN`), - title, status/priority pill, and two buttons: - - **Pick up ↗** → `sendPrompt('pick up the following ticket: T-NNN — ')` - - **Detail ↗** → `sendPrompt('show me T-NNN in detail')` - - Footer buttons: **Refresh board ↗** → `sendPrompt('/pql-board')`, - **Next batch ↗** → `sendPrompt('/whats-next')`. - - Widget rules: no emoji, sentence case, `↗` suffix on sendPrompt buttons. +2. Render with `mcp__visualize__show_widget`: load its `read_me` first + (`interactive` module) if not yet loaded this conversation, then pass the + script's stdout verbatim as `widget_code`. 3. **On receiving a "pick up the following ticket:" prompt** (from the panel or typed): treat it as single-ticket batch activation — the /whats-next diff --git a/.claude/skills/pql-board/support/make-panel b/.claude/skills/pql-board/support/make-panel new file mode 100755 index 000000000..4bde9f8c7 --- /dev/null +++ b/.claude/skills/pql-board/support/make-panel @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Fill panel-template.html with live pql data → widget_code for show_widget. + +Emits the interactive in-chat pql panel (see ../SKILL.md §Interactive panel): +WIP + unblocked leaf work of the active phase, capped, with one-click +Pick up ↗ / Detail ↗ sendPrompt actions. Claude runs this and pastes stdout +verbatim as the show_widget widget_code. + +Usage: .claude/skills/pql-board/support/make-panel [--phase T-NNN] [--cap N] + (defaults: active phase T-750, cap 10; output on stdout) +""" + +import argparse +import json +import subprocess +from pathlib import Path + +ARTIFACT_URL = "https://claude.ai/code/artifact/0064e59d-84a9-4a31-a830-f7d677df9d92" +FIELDS = "id,type,priority,status,title,updated_at" +PRIORITY_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3} +STATUS_RANK = {"in_progress": 0, "review": 1, "ready": 2, "backlog": 3} +ACTIONABLE_TYPES = ("story", "task", "bug") +MAINTENANCE = "T-1037" + + +def pql(*args): + out = subprocess.run( + ["pql", *args], capture_output=True, text=True, check=True + ).stdout + return json.loads(out) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--phase", default="T-750") + ap.add_argument("--cap", type=int, default=10) + args = ap.parse_args() + + wip = pql("ticket", "list", "--status", "in_progress", "--fields", FIELDS) + phase = pql( + "ticket", "list", "--under", args.phase, "--unblocked", "--leaf", + "--fields", FIELDS, + ) + maint = pql( + "ticket", "list", "--under", MAINTENANCE, "--unblocked", "--leaf", + "--fields", FIELDS, + ) + + seen, rows = set(), [] + for t in wip + phase + maint: + if t["id"] in seen or t["type"] not in ACTIONABLE_TYPES: + continue + if t["status"] in ("done", "cancelled"): + continue + seen.add(t["id"]) + rows.append(t) + + rows.sort(key=lambda t: t.get("updated_at") or "", reverse=True) + rows.sort( + key=lambda t: ( + STATUS_RANK.get(t["status"], 3), + PRIORITY_RANK.get(t.get("priority"), 2), + ) + ) + overflow = max(0, len(rows) - args.cap) + rows = rows[: args.cap] + + tpl = (Path(__file__).parent / "panel-template.html").read_text() + data = json.dumps( + {"rows": rows, "url": ARTIFACT_URL, "overflow": overflow}, + ensure_ascii=False, + separators=(",", ":"), + ).replace("</", "<\\/") + print(tpl.replace("__URL__", ARTIFACT_URL).replace("__DATA__", data)) + + +if __name__ == "__main__": + main() diff --git a/.claude/skills/pql-board/support/panel-template.html b/.claude/skills/pql-board/support/panel-template.html new file mode 100644 index 000000000..3278cecf5 --- /dev/null +++ b/.claude/skills/pql-board/support/panel-template.html @@ -0,0 +1,35 @@ +<h2 class="sr-only" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)">Interactive pql panel: work-in-progress and unblocked ready tickets with one-click pick-up and detail actions.</h2> +<div id="pp-rows" style="border:0.5px solid var(--border);border-radius:var(--radius);overflow:hidden;"></div> +<div id="pp-overflow" style="font-size:12px;color:var(--text-muted);margin-top:6px;"></div> +<div style="display:flex;gap:8px;margin-top:12px;"> + <button onclick="sendPrompt('/pql-board')" style="font-size:13px;"><i class="ti ti-refresh" style="font-size:15px;vertical-align:-2px" aria-hidden="true"></i> Refresh board ↗</button> + <button onclick="sendPrompt('/whats-next')" style="font-size:13px;">Next batch ↗</button> + <span style="flex:1"></span> + <a href="__URL__" style="font-size:13px;align-self:center;"><i class="ti ti-external-link" style="font-size:15px;vertical-align:-2px" aria-hidden="true"></i> Open board</a> +</div> +<script type="application/json" id="pp-data">__DATA__</script> +<script> +const D=JSON.parse(document.getElementById("pp-data").textContent); +const PR={critical:["var(--bg-danger)","var(--text-danger)"],high:["var(--bg-warning)","var(--text-warning)"], + medium:["var(--surface-1)","var(--text-secondary)"],low:["var(--surface-1)","var(--text-muted)"]}; +const ST={in_progress:["var(--bg-accent)","var(--text-accent)"],ready:["var(--bg-success)","var(--text-success)"], + backlog:["var(--surface-1)","var(--text-muted)"],review:["var(--bg-warning)","var(--text-warning)"]}; +document.getElementById("pp-rows").innerHTML=D.rows.map((t,i)=>{ + const p=PR[t.priority]||PR.medium, s=ST[t.status]||ST.backlog; + return `<div style="display:flex;align-items:center;gap:10px;padding:9px 12px;${i?"border-top:0.5px solid var(--border);":""}"> + <a href="${D.url}#${t.id}" style="font:500 13px var(--font-mono);">${t.id}</a> + <span style="font-size:14px;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${t.title.replace(/"/g,""")}">${t.title}</span> + <span style="font-size:11px;padding:2px 7px;border-radius:999px;background:${s[0]};color:${s[1]};">${t.status.replace("_"," ")}</span> + <span style="font-size:11px;padding:2px 7px;border-radius:999px;background:${p[0]};color:${p[1]};">${t.priority}</span> + <button data-act="pick" data-i="${i}" style="font-size:12px;padding:4px 8px;">Pick up ↗</button> + <button data-act="detail" data-i="${i}" style="font-size:12px;padding:4px 8px;">Detail ↗</button> + </div>`;}).join(""); +if(D.overflow>0) document.getElementById("pp-overflow").textContent= + `…and ${D.overflow} more unblocked — open the board for the full list.`; +document.querySelectorAll("#pp-rows button").forEach(b=>b.onclick=()=>{ + const t=D.rows[+b.dataset.i]; + sendPrompt(b.dataset.act==="pick" + ? `pick up the following ticket: ${t.id} — ${t.title}` + : `show me ${t.id} in detail`); +}); +</script>