diff --git a/.claude/skills/pql-board/SKILL.md b/.claude/skills/pql-board/SKILL.md new file mode 100644 index 000000000..ae73d33ba --- /dev/null +++ b/.claude/skills/pql-board/SKILL.md @@ -0,0 +1,59 @@ +--- +name: pql-board +description: > + Refresh (or first-create) the pql ticket-board Artifact β€” the clide-style + two-column board snapshot hosted at a stable claude.ai URL. Use when the user + says "refresh the board", "update the pql board", "/pql-board", or asks to see + the ticket board in the desktop app. +--- + +# pql Board Artifact + +The board is a self-contained interactive HTML snapshot of the pql ticket store +(status-grouped rail, ticket dossier with dependencies/children, filter, +deep-links), deployed as a claude.ai Artifact. Artifacts are network-sandboxed, +so the page is a **snapshot** β€” this skill regenerates and redeploys it. + +**Canonical URL (redeploy target β€” keep stable so the user's open tab survives):** + + https://claude.ai/code/artifact/0064e59d-84a9-4a31-a830-f7d677df9d92 + +## Workflow + +1. Generate the fragment (from the repo root; pql must resolve to this vault β€” + run from the MAIN checkout, not a worktree): + + ```bash + tooling/pql-board-html /pql-board.html + ``` + + The script pulls `pql ticket list --full`, batch-fetches blocker edges for + non-terminal tickets, and `pql plan status` for the header. Output is an + Artifact-ready fragment (no doctype/html/head/body). + +2. Deploy with the **Artifact tool**, always passing the canonical URL so the + existing artifact updates in place (a fresh session without `url` would mint + a new address and orphan the user's tab): + + - `file_path`: the generated file + - `url`: the canonical URL above + - `favicon`: `πŸ—‚οΈ` (keep stable β€” the user finds the tab by it) + - `label`: short, e.g. `board-YYYY-MM-DD` + +3. Reply with one line: ticket/edge counts from the generator's stdout + the + URL. The user reloads their tab. + +## Notes + +- If the Artifact call 409s (another session touched it), reconcile then retry + with `force` only if intentionally replacing. +- If the canonical artifact was deleted and a new URL gets minted, **update the + URL in this skill file** in the same turn and commit it. +- The generator is `tooling/pql-board-html` (python, pql-native JSON only). + Known scope limits vs clide: no decisions view, no kanban columns, no live + editing. Dep edges cover non-terminal tickets only β€” if pql grows + `ticket list --with-blockers`, simplify the script (candidate upstream FR + per the pql-gaps-go-upstream rule). +- Companion interaction surface: the visualize widget (`show_widget`) supports + `sendPrompt()` β€” for in-chat quick actions (refresh button, WIP strip); see + the session pattern, not this skill. diff --git a/tooling/pql-board-html b/tooling/pql-board-html new file mode 100755 index 000000000..7fda8cd0c --- /dev/null +++ b/tooling/pql-board-html @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +"""Render the pql ticket board as a self-contained interactive HTML snapshot. + +Companion to clide's two-column board view, for sessions running in the Claude +desktop app: the output is deployed as a claude.ai Artifact (stable URL across +redeploys), giving always-visible pql board + ticket-detail reference without +clide. The page is fully client-side over embedded JSON β€” artifacts are +network-sandboxed, so this is a *snapshot*; regenerate + redeploy to refresh. + +Data comes from pql's native JSON output only (no output parsing): + - `pql ticket list --full` β†’ every ticket, whole rows + - `pql ticket show --with-blockers` β†’ dep edges, batched, for + non-terminal tickets only (native list output carries no dep edges β€” if + pql ever grows `list --with-blockers`, drop the batching here; candidate + upstream feature request per the no-ad-hoc-wrappers rule) + - `pql plan status` β†’ header counts + +Usage: tooling/pql-board-html [OUTPUT.html] + (default output: /tmp/pql-board.html) + +The emitted file is an Artifact-ready fragment: no doctype/html/head/body +wrapper (the Artifact pipeline adds those); it starts with + <style>. +""" + +import html +import json +import subprocess +import sys +from datetime import datetime, timezone + +NON_TERMINAL = ["in_progress", "review", "ready", "backlog"] +SHOW_CHUNK = 40 + + +def pql(*args): + out = subprocess.run( + ["pql", *args], capture_output=True, text=True, check=True + ).stdout + return json.loads(out) + + +def fetch(): + tickets = pql("ticket", "list", "--full") + plan = pql("plan", "status") + + ids = [t["id"] for t in tickets if t.get("status") in NON_TERMINAL] + blockers = {} + for i in range(0, len(ids), SHOW_CHUNK): + chunk = ids[i : i + SHOW_CHUNK] + shown = pql("ticket", "show", ",".join(chunk), "--with-blockers") + if isinstance(shown, dict): + shown = [shown] + for row in shown: + edges = row.get("blockers") or row.get("blocked_by") or [] + blockers[row["id"]] = [ + e["id"] if isinstance(e, dict) else e for e in edges + ] + return tickets, blockers, plan + + +PAGE = """<title>Settled Reach β€” pql board + + +
+

Settled Reach Β· pql

+ + snapshot __STAMP__ +
+
+
+
+ +
+
+
+
+
Select a ticket.
+
+ + + +""" + + +def main(): + out_path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/pql-board.html" + tickets, blockers, plan = fetch() + stamp = datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M") + data = json.dumps( + {"tickets": tickets, "blockers": blockers, "plan": plan}, + ensure_ascii=False, + separators=(",", ":"), + ).replace("