diff --git a/.claude/skills/clean-house/.pql-install.json b/.claude/skills/clean-house/.pql-install.json new file mode 100644 index 00000000..f4d08701 --- /dev/null +++ b/.claude/skills/clean-house/.pql-install.json @@ -0,0 +1,5 @@ +{ + "version": "1.5.0", + "hash": "sha256:8090b99552c828ce019aa3740d8a0acb4768a443704b4e935efa23b32e91128d", + "installed_at": "2026-06-02T08:46:25Z" +} diff --git a/.claude/skills/clean-house/SKILL.md b/.claude/skills/clean-house/SKILL.md new file mode 100644 index 00000000..de63c433 --- /dev/null +++ b/.claude/skills/clean-house/SKILL.md @@ -0,0 +1,229 @@ +--- +name: clean-house +description: > + Run a documentation-discipline pass over a DQR (Decisions / Questions / + Rejected) markdown system. Audits decisions/, surfaces drift (broken + anchor links, missing Q→D backlinks, sunset-shaped phrases without + linked tickets, files over the split threshold, dead cross-references), + batches findings by category, and uses AskUserQuestion to apply fixes + interactively. Trigger this skill whenever the user asks to clean, + tidy, audit, sweep, garden, or review the state of decisions/, the + DQR system, the questions index, or the records under decisions/. + Also trigger before a release, after a session that touched many + records, or whenever the user mentions documentation drift, stale + plans, or "house" / "clean-house" / "house cleaning". Imperative — + apply fixes, not just report. +--- + +# clean-house + +A periodic gardening pass over `decisions/`. The DQR system is +markdown-as-source-of-truth; `.pql/pql.db` is a derived index. This skill +operates on the markdown files, batches drift by category, and uses +`AskUserQuestion` to decide what to apply. + +The skill is **imperative**. It really cleans. The verb-noun match is +load-bearing — a `clean-house` that only produced a report would be +misnamed. Mechanical fixes get a single batched approval, judgment-call +findings get individual prompts, and a "stop asking" escape hatch is +always present. + +You — Claude — are the runtime. Read this file, read +`references/rules.md`, follow the procedure below, call `pql` and `Edit` +to do the work, call `AskUserQuestion` for the prompts. + +## When to run + +- Before a release. +- After a session that touched many D / Q / R records. +- When something feels stale and a sweep is wanted. +- When `pql decisions validate` is green but `decisions/` still feels off. + +This skill is **not** a replacement for `pql decisions validate`. +Validate is fast, hot, and gates pre-push. clean-house is slow, cold, +on-demand. Escalation path: rules that fire often here are candidates +for promotion to `pql decisions validate`; rules that almost never fire +stay here where false positives don't slow anyone down. + +## Procedure + +1. **Gate on validate.** Run `pql decisions validate`. If it exits + non-zero, stop and tell the user to fix validator findings first. + Do not proceed. + +2. **Load the rule catalog.** Read `references/rules.md`. Each rule + names its detection, fix, and finding-id format. + +3. **Probe conventions.** Read `decisions/.clean-house.yaml` if it + exists; otherwise infer from the project. Conventions to resolve: + - `heading_level`: 2 or 3 — the depth records use (`## D-N` vs + `### D-N`). Probe by sampling the first 3 records' file + locations and looking for the first matching ATX heading. + - `backlink_phrasing`: the project's Q→D backlink phrase (default + `Resolved → D-N`; some projects use `Resolved as D-N` or + `→ D-N`). + - `file_threshold`: default 350 (RULE-FILE-OVER-THRESHOLD). + - `stale_open_q_days`: default 60 (RULE-STALE-OPEN-Q). + + Pass the resolved conventions into each rule's detection. Cache + the probe result for the run. + + `decisions/.clean-house.yaml` shape (all keys optional): + ```yaml + heading_level: 3 + backlink_phrasing: "Resolved → " + file_threshold: 500 + stale_open_q_days: 90 + exclude_paths: ["legacy/", "drafts/"] + ``` + +4. **Sync the index.** Run `pql decisions sync` so the DB reflects + the markdown. + +5. **Walk decisions/.** Use `pql decisions list` to enumerate + records, plus `pql decisions read ` per record when a rule + needs the body. For file-level rules (size, sort), read the + markdown files directly. **Filter out** any record whose + `file_path` begins with `legacy/` (or any prefix in + `exclude_paths` from the probe). + +6. **Run each rule's detection.** Collect findings. Tag each with + its rule ID, finding ID (per the rule's format), category + (`mechanical` | `judgment`), and the record(s)/file(s) involved. + +7. **Group findings by rule** and prompt. Honor any "skipped 3+ + times" findings from the skip ledger by escalating their prompt + ("skipped 3x — promote to manual decision, downgrade to + summary-only, or keep asking?"): + + - **Mechanical rules** — one prompt per rule, batched. Show the + count and offer: apply all / show diff first / skip this rule / + **stop asking** (global — see below). + - **Judgment rules** — one prompt per finding. Options match the + rule's `Fix` section, plus the same global **stop asking**. + + **"Stop asking" semantics (global).** When the user picks "stop + asking" on any prompt, immediately halt all further prompts in + the run — mechanical batches not yet asked AND remaining + judgment findings. Move all unprompted findings to the skip + ledger marked `deferred-stop-asking`. Skip to step 9. + + **"Show diff first" loop.** When the user picks "show diff + first" on a mechanical batch, emit a unified diff of the staged + edits (one block per file), then re-prompt the same question + with the same options minus "show diff first" — preventing + infinite loops while still letting the user inspect before + committing. + +8. **Apply approvals.** For mechanical rules use `Edit` to mutate + the markdown directly. For judgment rules, follow the option the + user picked (file a ticket via `pql ticket new`, mark superseded + by editing the record, etc.). + +9. **Update the skip ledger and history.** Both files live at + `decisions/.clean-house-state.md`, gitignored. The file uses + per-run section headers so it doubles as run history: + + ```markdown + # clean-house run history + + ## 2026-05-06T13:42Z + fired: RULE-ANCHOR-DRIFT(1) RULE-SUNSET-WITHOUT-TICKET(1) + applied: RULE-ANCHOR-DRIFT(1) RULE-SUNSET-WITHOUT-TICKET(1) + skipped: + - RULE-ANCHOR-DRIFT D-8:#q-1-markdown-mirror-for-tickets + + ## 2026-05-04T11:10Z + fired: ... + ``` + + Each section starts with a UTC ISO-8601 timestamp. `fired` + counts findings detected; `applied` counts findings the user + acted on; `skipped` lists ` ` pairs. + + **Promotion-candidate computation.** Scan the most recent N=7 + sections (or all sections if fewer). A rule is a promotion + candidate if it `fired` in ≥ 5 of those sections. Surface the + candidates in the summary; do not auto-promote. + +10. **Emit the summary** (see Output below). + +## Question phrasing + +Always include the rule ID in the prompt so the user learns what's +being checked. + +**Mechanical (batched):** +> RULE-ANCHOR-DRIFT: 12 cross-reference anchors point to headings that +> no longer exist (renamed or deleted). Apply all 12 fixes? Options: +> apply all / show diff first / skip this rule / stop asking, summarize +> the rest. + +**Judgment (individual):** +> RULE-SUNSET-WITHOUT-TICKET: D-59 mentions "must track dugite-native +> releases for security updates" but has no linked T. Options: file T +> now / draft a T description first / mark as already covered (add note +> to D) / skip / stop asking. + +Phrasing rules: + +- **Always include "stop asking, summarize the rest."** Sessions + without bandwidth for full review need an escape. Forcing answers + to every question kills the tool. +- **Always include "skip."** Skipped findings go to the ledger; three + consecutive skips of the same finding promote the next prompt. +- **Mechanical batches; judgment doesn't.** "Apply all 12 fixes?" is + one decision against a clear delta. Twelve individual prompts defeat + the point. Conversely, judgment findings are each separate work — + presenting them as a batch hides cost. + +## What this skill does NOT do + +- Does not file tickets without asking. +- Does not rewrite D-record body prose, only metadata fields and + cross-reference links. +- Does not touch records under `legacy/`. +- Does not run `pql decisions validate` *as a fix* — only as a + precondition gate. +- Does not promote rules to `pql decisions validate` automatically. + Promotion is a human decision; this skill only flags candidates + ("RULE-X has fired in 5 of the last 7 runs — consider promoting"). + +## Non-interactive context + +If invoked without an interactive `AskUserQuestion` surface (a CI run, +a batch script), refuse to apply judgment fixes. Apply mechanical +fixes only if the user explicitly says so via the trigger phrase +("auto-apply mechanical, skip judgment"); otherwise emit the summary +and exit without mutating files. + +## Output + +At the end, emit a summary block: + +``` +clean-house — sweep complete +──────────────────────────── +Files scanned: 11 +Records parsed: 62 D, 23 Q, 11 R +Conventions: heading_level=3 backlink="Resolved → " +Mechanical fixes: 14 applied, 0 deferred +Judgment findings: 3 acted on, 2 deferred (in state file) +Promotion candidates: RULE-ANCHOR-DRIFT (5/7 runs) +Touched files: decisions/architecture.md, decisions/questions.md +``` + +If this is the first run (no prior history) or there are fewer +than 5 prior runs, `Promotion candidates:` reports +`(none — N/7 runs of history)` instead of decorative empty content. + +The summary names the touched files so the next commit message can be +honest about what changed. The skill does not commit on its own. + +## Versioning + +clean-house ships embedded in the pql binary; its version is the +pql version (`pql --version`). To see what changed and when, read +`CHANGELOG.md` or `git log internal/skill/clean-house/` in the pql +repo. Keep this file and `references/rules.md` consistent with +each other when iterating — they're the contract. diff --git a/.claude/skills/clean-house/references/rules.md b/.claude/skills/clean-house/references/rules.md new file mode 100644 index 00000000..85d8edc3 --- /dev/null +++ b/.claude/skills/clean-house/references/rules.md @@ -0,0 +1,386 @@ +# clean-house rule catalog + +The rules clean-house runs and the reasoning behind each. New rules +land here as their own entry. Catalog churn — additions, refinements, +retirements — is tracked in pql's `CHANGELOG.md` and `git log +internal/skill/clean-house/`. When a rule is retired, leave a +**Retired** stub (ID + one-line reason + retirement commit) so the +trail of "we used to check X" is recoverable from this file alone. + +## Reading a rule + +Each entry below has: + +- **ID** — Stable identifier (e.g. `RULE-ANCHOR-DRIFT`). Used in + AskUserQuestion prompts and skip-ledger entries so the user learns + what's being checked. +- **Category** — `mechanical` or `judgment`. Drives whether findings + batch into one prompt or each get their own. +- **Finding ID** — Format string for the per-finding stable + identifier. Skip-ledger entries are written as ` + `; subsequent runs use the same format to + recognize "same finding skipped 3 runs in a row" and escalate. + Use only stable inputs (record IDs, file paths, slug strings, body + hashes) — never line numbers or timestamps. +- **Detection** — Concrete steps to find violations. Names the + pql command or file primitive used. +- **Fix** — Mechanical: deterministic action. Judgment: the prompt + option list and what each option does. +- **Why** — One paragraph on the failure mode this rule guards + against. Future-you reading the rule benefits from the reasoning, + not just the check. + +--- + +## RULE-ANCHOR-DRIFT + +**Category:** mechanical + +**Finding ID:** `:` (e.g. +`D-8:#q-1-markdown-mirror-for-tickets` or +`D-8:questions.md#q-1`). + +**Detection:** + +Anchor-only markdown links (`[text](#slug)`) resolve against the +**source file's full heading set**, not the body of a single record +— record-level headings (`### D-N: …`) live as siblings in the +file and are valid link targets. The previous detection (which used +`pql decisions read`'s body-only `headings` array) missed these +and produced false positives. + +Per source body: + +1. Open the source body's file (`/` from + `pql decisions list`) and extract every ATX heading. Build a + slug index for the file using the GFM convention (lowercase, + hyphenate spaces, drop punctuation, disambiguate duplicates with + `-1`/`-2`). Cache per file — every record in that file uses the + same index. +2. For each `[text](target)` link in the body: + - Anchor-only (`#slug`): check `slug` against the **source file's** + index. Missing → flag. + - Cross-file (`path.md#slug`): resolve `path.md` relative to the + source file's directory; check `slug` against that file's + index. Missing file or missing slug → flag. + +**Fix:** + +If the slug exists in a sibling file's index (same directory) and +the link is anchor-only, rewrite to `path.md#slug` (the canonical +cross-file form). If the slug exists in the source file's index +with edit-distance ≤ 2 from the link target, rewrite to the +matched slug. Otherwise downgrade to judgment ("no auto-fix; the +heading was removed, not renamed") with options: drop the link / +point elsewhere / mark as intentionally dangling. + +**Fix:** + +If exactly one heading with a closely-matching slug exists (slug +edit-distance ≤ 2 or substring match), rewrite the link to point at +that heading. If no plausible match exists, downgrade the finding to +judgment ("no auto-fix; the heading was removed, not renamed") and +prompt the user with options: drop the link / point elsewhere / mark +as intentionally dangling. + +**Why:** + +When a heading is renamed for clarity, every cross-reference pointing +at the old slug silently breaks. The rendered docs still look fine — +the link just goes nowhere. Without periodic sweeps these decay +indefinitely; the cost of the sweep is small compared to the cost of +a reader following a dead link and losing trust in the index. + +--- + +## RULE-MISSING-Q-BACKLINK + +**Category:** mechanical + +**Finding ID:** `:` (e.g. `D-7:Q-2`). Order is always +D-first regardless of which side is missing the backlink. + +**Detection:** + +For each D record body, find lines of the shape `Resolves: Q-N` +(plain text, typically in the metadata block at the top of the +record). For each Q-N referenced, fetch that Q record's body via +`pql decisions read Q-N` and check whether it contains a line of the +shape `Resolved → D-N` (or equivalent backlink phrasing — match the +project's own convention; default pattern is `Resolved → D-N`). + +Also check the reverse direction: any Q record claiming +`Resolved → D-N` whose D-N body lacks `Resolves: Q-N`. + +**Fix:** + +Insert the missing backlink in-place using `Edit`: + +- Missing on the D side: add `Resolves: Q-N` to the D's metadata + block (typically right after `Domain:` / `Status:` lines). +- Missing on the Q side: append `Resolved → D-N` to the Q's status + line (or in the conventional position for that project). + +**Why:** + +Bidirectional Q↔D links are the navigation backbone of the DQR +system. When one side drifts, search-by-decision finds nothing for +that question and search-by-question doesn't surface its resolution. +The asymmetric state usually arises from a hand-edit on one record +that forgot to update the other; the fix is purely mechanical because +the correct content is already determined by the existing pointer +in the other direction. + +--- + +## RULE-RECORD-SORT + +**Category:** mechanical + +**Finding ID:** `` (e.g. `decisions/architecture.md`). +The whole file is one finding — sort applies to the file as a +unit. + +**Detection:** + +For each `decisions/*.md` file, read it directly (no pql) and find +all top-level `## D-N` / `## Q-N` / `## R-N` headings in order. Strip +the prefix, parse the numeric suffix, and check whether the sequence +is monotonically ascending within each ID family (D, Q, R kept +separate — files commonly mix families). + +Only flag a file if it is **otherwise tidy** — currently no out-of- +order amendments interleaved with new records. The heuristic: if the +sort would touch fewer than three records out of position, apply it; +if more, downgrade to judgment ("this file looks deliberately +arranged — confirm before reordering"). Three is a soft threshold; if +the file's last commit message contains `WIP` or `do-not-sort`, skip. + +**Fix:** + +Reorder the records within the file so each ID family is ascending. +Preserve everything else (headings between record blocks, any prose +prelude/postlude). Use `Edit` with full block replacement, not in- +place line shuffling — easier to verify the diff. + +**Why:** + +Records added at the bottom of a file are easy to write but hard to +find. Sorted IDs let a reader find D-37 by jumping to the +two-thirds mark of the file rather than scanning. The cost of the +sort is one reordering pass; the benefit is paid back on every +subsequent read. + +--- + +## RULE-EOF-NORMALIZATION + +**Category:** mechanical + +**Finding ID:** `` (one finding per file). + +**Detection:** + +For each `decisions/*.md` file (and any other markdown the skill +touched during this run), read directly. Flag if: + +- File does not end with exactly one `\n`. +- Any line contains trailing whitespace before its `\n`. + +**Fix:** + +Trim trailing whitespace from each line. Ensure exactly one trailing +newline at end of file. Use `Edit` only if a violation was found — +this rule must not produce a no-op diff. + +**Why:** + +Editor and git config drift causes whitespace creep that's invisible +in rendering but pollutes diffs (every record edit ends up touching +unrelated lines). A periodic normalization keeps future diffs clean +without forcing per-editor enforcement on every contributor. + +--- + +## RULE-SUNSET-WITHOUT-TICKET + +**Category:** judgment + +**Finding ID:** `:` where phrase-hash-8 is +the first 8 hex chars of `sha256()`. Lets the +ledger distinguish two sunset phrases in the same record. + +**Detection:** + +For each D record body (via `pql decisions read `), grep for +sunset-shaped phrases. Default regex set: + +``` +(?i)\b(delete|remove|sunset|kill[ -]?switch|tear[ -]?down) when\b +(?i)\bmust (track|monitor|watch|follow)\b +(?i)\brevisit (when|after|once)\b +(?i)\b(deprecate|retire) (when|after|once)\b +``` + +For each match, run `pql ticket list --decision `. If no tickets +exist, the D has work-shaped intent without a tracked T — flag. + +**Fix (prompt options):** + +- **File T now** — Run `pql ticket new task "" --decision `, + then prompt the user for a description (or call `pql ticket refine + write --description ...` after creation). +- **Draft a T description first** — Open an `AskUserQuestion` for the + description, then file as above. +- **Mark as already covered** — Add a note to the D body + ("Tracked under T-N") via `Edit`. Skill does not assert which T; + user provides the ID. +- **Skip** — Adds to the ledger. +- **Stop asking** — Skip remaining sunset findings, summarize. + +**Why:** + +Sunset-shaped intent ("we'll handle X when Y happens") is the most +common source of accumulated debt in a DQR system: the trigger +condition arrives, nobody remembers the D, the work doesn't happen. +Linking each sunset to a T is the simplest defense — a T is a +backlog item that surfaces in `plan whatsnext` / `plan board`. The +fix is judgment because the right action depends on whether the D's +condition is still relevant, whether it's already covered, and what +the right scope is for the resulting T. + +--- + +## RULE-FILE-OVER-THRESHOLD + +**Category:** judgment + +**Finding ID:** `` (one finding per file; threshold +choice is judgment, not a per-line problem). + +**Detection:** + +For each `decisions/*.md` file, count lines (via `wc -l` or +equivalent). Default threshold: **350 lines**. Configurable per +project — read `decisions/.clean-house.yaml` if present, key +`file_threshold`. Fall back to 350. + +**Fix (prompt options):** + +- **Split now (which axis?)** — Prompt for the split axis: by ID + family (D/Q/R), by domain, by date range, custom. Then perform the + split: create new file(s), move records, update any anchor links + pointing into the moved records (run RULE-ANCHOR-DRIFT in apply + mode against the affected files after the split). +- **Accept and raise the threshold** — Update + `decisions/.clean-house.yaml`'s `file_threshold` to the next + reasonable round number above the current line count. +- **Defer** — Skip until next run. + +**Why:** + +A single decisions file growing past ~350 lines is the point at which +linear scanning starts to lose to grep, and where the cost of +splitting (renaming anchors) is still small. Beyond ~600 lines the +split cost compounds. The threshold is judgment because some projects +deliberately keep one file per domain and accept the size; others +split aggressively. The skill surfaces the question; it doesn't +decide. + +--- + +## RULE-DEAD-FILE-REFERENCE + +**Category:** judgment + +**Finding ID:** `:` (path token as written +in the body, not resolved). + +**Detection:** + +For each D record body (via `pql decisions read `), grep for +path-shaped tokens. Default regex: + +``` +\b([\w./-]+\.(md|go|py|sql|yaml|yml|toml))\b +``` + +The first detection pass produced 6/6 false positives in +real-world use; the regex is necessary but not sufficient. For +each match, apply the filters below in order — if any matches, +**skip without flagging**: + +1. **Placeholder filter.** Token contains `T-NNN`, `D-NNN`, + `Q-NNN`, `R-NNN`, `...`, `<`, `>`, or `*` — it's a pattern, + not a real path. +2. **Source-relative resolution.** Resolve the token against the + source file's directory (`/`'s dir). If + `[ -e ]`, the reference is live. +3. **Repo-relative resolution.** If `[ -e ]` from the + repo root, the reference is live. +4. **Basename-fallback.** Run `find -name ` + (where basename is the last path component). If exactly one + match exists, treat the reference as live and emit a low- + priority "consider rewriting to the absolute path" note (not a + judgment finding). If multiple matches exist, do not flag — + the token is too ambiguous. + +Only after all four filters miss does the reference qualify as +truly dead and warrant a judgment prompt. + +**Fix (prompt options):** + +- **Update reference** — Prompt for the new path; rewrite the + reference via `Edit`. If the user types a path, validate it exists + before applying. +- **Mark superseded** — Add a note to the D ("This decision + references files no longer present; the underlying constraint + was retired by D-N") and prompt for the superseding D ID. +- **Defer** — Skip until next run. + +**Why:** + +Decisions reference code paths to ground their reasoning in the +codebase that motivated them. When the code moves or gets deleted, +the D's reasoning becomes harder to verify. Dead refs are a signal +that either the decision should be updated or the decision itself is +no longer load-bearing — both are judgment calls the user needs to +make. + +--- + +## RULE-STALE-OPEN-Q + +**Category:** judgment + +**Finding ID:** `` (one finding per Q-record; staleness +threshold is global per run, so a Q is either stale or not). + +**Detection:** + +Run `pql decisions list --type question --status open`. For each +returned record, check the `date` field. Default staleness threshold: +**60 days** (configurable via `decisions/.clean-house.yaml` key +`stale_open_q_days`). Flag any Q with `date` older than the +threshold. + +**Fix (prompt options):** + +- **Still relevant** — Touch the Q's `date` field to today, optionally + add a one-line "still open as of YYYY-MM-DD: " to the body. +- **Nudge to load-bearing index** — If the project keeps a + load-bearing Qs list (a curated index of unresolved questions + affecting current work), prompt for inclusion and add the Q there. +- **Mark withdrawn** — Change the Q's status to `withdrawn` (the DQR + system's terminal state for "no longer worth answering"); the + user supplies a one-line reason. +- **Defer** — Skip until next run. + +**Why:** + +Open questions are useful when they're current; stale ones become +ambient noise that drowns the signal of new questions. Most +projects don't enforce explicit Q-lifecycle, so without a periodic +prompt the open list grows forever. The right action depends on +whether the question is still open in fact — not just in metadata — +which only the user can confirm. diff --git a/.claude/skills/commit/SKILL.md b/.claude/skills/commit/SKILL.md new file mode 100644 index 00000000..a5d390a1 --- /dev/null +++ b/.claude/skills/commit/SKILL.md @@ -0,0 +1,53 @@ +--- +name: commit +description: Create a well-formatted git commit with staged changes +--- + +# Git Commit + +Create a well-formatted commit with staged changes following best practices. + +## Steps + +1. Run `git status --porcelain` to check for changes +2. If no staged changes, show unstaged files and ask what to stage +3. Run `git diff --cached` to review staged changes +4. Generate a commit message following Conventional Commits format: + - `feat:` new feature + - `fix:` bug fix + - `docs:` documentation + - `refactor:` code restructuring + - `test:` adding tests + - `chore:` maintenance +5. Create commit with the message, adding Co-Authored-By trailer + +## Commit Message Format + +``` +(): + + + +Co-Authored-By: Claude +``` + +## Best Practices + +- Warn about large commits (>500 lines changed) +- Suggest splitting large changes into smaller commits +- Never skip pre-commit hooks unless explicitly requested + +## Changelog + +After committing, update `CHANGELOG.md` following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format: + +1. Add entry under `[Unreleased]` section (create if not exists) +2. Categorize changes: + - `Added` - new features + - `Changed` - changes to existing functionality + - `Deprecated` - features marked for removal + - `Removed` - removed features + - `Fixed` - bug fixes + - `Security` - security-related changes +3. Write entries in imperative mood: "Add feature" not "Added feature" +4. Reference issue numbers where applicable diff --git a/.claude/skills/penpot-login/SKILL.md b/.claude/skills/penpot-login/SKILL.md new file mode 100644 index 00000000..9d9207c3 --- /dev/null +++ b/.claude/skills/penpot-login/SKILL.md @@ -0,0 +1,104 @@ +--- +name: penpot-login +description: Connect to Penpot MCP - starts the MCP server, logs into penpot.schweitz.net, and installs/connects the plugin +allowed-tools: Read, Bash, mcp__chrome-devtools__new_page, mcp__chrome-devtools__navigate_page, mcp__chrome-devtools__take_snapshot, mcp__chrome-devtools__take_screenshot, mcp__chrome-devtools__click, mcp__chrome-devtools__fill, mcp__chrome-devtools__press_key, mcp__chrome-devtools__list_pages +--- + +# Penpot MCP Connection + +This skill connects Claude to Penpot by: +1. Starting the Penpot MCP server (if not running) +2. Logging into penpot.schweitz.net via Authentik +3. Installing and connecting the Penpot MCP plugin + +## Step 1: Start MCP Server + +Check if the MCP server is running, start it if not: + +```bash +# Check if server is running +if ! curl -s http://localhost:9880/mcp > /dev/null 2>&1; then + # Start the server in background + cd ~/Projects/penpot-mcp && ./start-server.sh & + sleep 3 # Wait for server to start +fi +``` + +Server endpoints when running: +- MCP Server: http://localhost:9880/mcp +- Plugin Server: http://localhost:9879/ +- WebSocket: ws://localhost:4402 + +## Step 2: Login Credentials + +Read credentials from the project root file `claude-authentik-credentials.md`: +!`cat claude-authentik-credentials.md` + +## Step 3: Login Flow + +1. **Navigate to Penpot** + - Use `mcp__chrome-devtools__new_page` to go to `https://penpot.schweitz.net` + +2. **Check if already logged in** + - Take a snapshot + - If you see "Projects" heading, you're logged in - skip to Step 4 + - If you see login page, continue with authentication + +3. **Authenticate via Authentik** (if not logged in) + - Click the OpenID button to redirect to Authentik + - On auth.schweitz.net, fill username textbox + - Click "Log in" button + - Fill password textbox + - Click "Continue" button + - If redirected to Authentik user page instead of Penpot, navigate back to `https://penpot.schweitz.net` + +## Step 4: Open Design File + +1. **Navigate to the Clide project** + - Double-click on the TUI file to open the workspace + +## Step 5: Install/Connect Plugin + +1. **Open Plugins menu** + - Click the Plugins button (puzzle icon, keyboard shortcut: Cmd+Alt+P) + +2. **Check if plugin is installed** + - If "Penpot MCP Plugin" appears under "INSTALLED PLUGINS", click OPEN + - Otherwise, install it first: + +3. **Install plugin** (if not installed) + - Fill the plugin URL textbox with: `http://localhost:9879/manifest.json` + - Click INSTALL + - Click ALLOW on the permissions dialog + +4. **Connect to MCP server** + - In the plugin UI, click "CONNECT TO MCP SERVER" + - Verify it shows "Connected to MCP server" + +## Step 6: Configure Claude Code MCP + +Add the Penpot MCP server to Claude Code (if not already configured): + +```bash +claude mcp add penpot -t http http://localhost:9880/mcp +``` + +**Important**: Use HTTP transport (`-t http`) with the `/mcp` endpoint. Do NOT use SSE transport - it causes "Server not initialized" errors. + +## Step 7: Verify Connection + +After connecting, restart the MCP connection in Claude Code: +- Use `/mcp` command to reconnect the penpot server +- The Penpot MCP tools (execute_code, export_shape, etc.) will then be available + +Test with: +```javascript +mcp__penpot__execute_code(code="return penpot.currentPage.name;") +``` + +## Important Notes + +- Keep the Penpot plugin UI window open while using MCP tools +- The MCP server must be running for the plugin to connect +- If connection fails, check that the server is running on port 9880 +- Use HTTP transport (`-t http`), NOT SSE transport