From bdd1b319bc128addcdff4696d0d1a54f6512c46b Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Fri, 24 Jul 2026 12:51:18 +0200 Subject: [PATCH] =?UTF-8?q?feat(skills):=20pql-board=20=E2=80=94=20clide-s?= =?UTF-8?q?tyle=20ticket=20board=20as=20a=20stable=20claude.ai=20Artifact?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tooling/pql-board-html: self-contained interactive board snapshot from pql-native JSON (ticket list --full + batched --with-blockers for non-terminal tickets + plan status) — status-grouped rail with filter/type chips, ticket dossier with status pills, dep/children chips, deep links, keyboard nav; clide's visual identity (amber on warm near-black, mono data type). The /pql-board skill regenerates and redeploys to the canonical artifact URL so the user's open tab survives refreshes across sessions. Co-Authored-By: Claude Fable 5 --- .claude/skills/pql-board/SKILL.md | 59 +++++ tooling/pql-board-html | 347 ++++++++++++++++++++++++++++++ 2 files changed, 406 insertions(+) create mode 100644 .claude/skills/pql-board/SKILL.md create mode 100755 tooling/pql-board-html 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("