Prune AI conventions and stale skills
Voice rules duplicated writing-style.md. Model-selection now names
when Opus is wrong. Deleted skills were thin wrappers or unused.

Assisted-by: Claude Opus 4.7 via Claude Code
change lqvmryzmlmxqmzwylvnvxwrorkqoynmm
commit 38562c6996bec112822962dcf116434d76927b77
author Alpha Chen <alpha@kejadlen.dev>
date
parent lzuolvtk
diff --git a/ai/_AGENTS.md b/ai/_AGENTS.md
index 68a48816..b1993895 100644
--- a/ai/_AGENTS.md
+++ b/ai/_AGENTS.md
@@ -48,36 +48,9 @@ version," not "file has newer version."
 
 ## Writing voice
 
-When writing on my behalf, match these patterns. For the full
-reference with examples, see `ai/references/writing-style.md` in my
-dotfiles repo.
-
-Baseline: First person, casual-professional, direct. Contractions
-welcome. Concise — content only, no filler or sign-offs.
-
-Sentence structure: Default to short, punchy sentences. Use dashes for
-asides — not semicolons. Parentheses for softening qualifiers ("for
-me, at least"). Drop subjects in casual contexts ("Would prefer to
-walk" not "I would prefer to walk").
-
-Opinions: Lead with "I think we should..." for proposals, "I feel
-like..." for softer takes. When disagreeing, propose an alternative
-rather than just objecting. When frustrated, lean into dry humor.
-
-Punctuation: Periods optional on short messages. Exclamation marks
-genuine but sparing. Use backticks for inline code even in casual
-messages. Link to PRs, code, and threads rather than describing them.
-
-Discovery markers: "Huh," "Ah," "Oh," and "Ooh" are natural openers —
-"Huh" for surprise/curiosity, "Ooh" for enthusiasm, "Ah/Oh" when
-figuring something out.
-
-Greetings: `:wave:` alone, not "Hey" or "Hi." No sign-offs — messages
-end when the thought ends. "Thanks!" (one word) after being helped.
-
-Tone scales with audience: fragments and lowercase in DMs, full
-sentences in channels, parenthetical empathy-checks for cross-team
-messages.
+When writing on my behalf in Slack, DMs, or similar informal contexts,
+load `ai/references/writing-style.md` and match it. Don't apply voice
+rules to code, commits, PRs, or docs — those have their own sections.
 
 ## Documentation
 
diff --git a/ai/claude/rules/model-selection.md b/ai/claude/rules/model-selection.md
index 6577bfb1..16b21c16 100644
--- a/ai/claude/rules/model-selection.md
+++ b/ai/claude/rules/model-selection.md
@@ -18,3 +18,19 @@ instructions are clear and the task is bounded.
 
 Commit messages, simple lookups, formatting, mechanical edits, generating
 boilerplate. Anywhere speed matters more than depth.
+
+## When to question the default
+
+Opus is wrong when:
+- The task is bounded and well-specified — Sonnet finishes faster at
+  near-identical quality. Long Opus sessions on mechanical work waste
+  latency and cost.
+- You're iterating in a tight loop (manual edits, repeated small
+  prompts). Sonnet's speed compounds.
+- You're using Fast mode out of habit but the work is exploratory —
+  the speed bias is hiding under-thought design.
+
+Signals to drop a level mid-session:
+- You're correcting Claude on simple facts more than once.
+- The remaining work is mechanical follow-through on a decided plan.
+- You're paying for a 1M context window on a 20k-token task.
diff --git a/ai/skills/apple-container/SKILL.md b/ai/skills/apple-container/SKILL.md
deleted file mode 100644
index fac88284..00000000
--- a/ai/skills/apple-container/SKILL.md
+++ /dev/null
@@ -1,84 +0,0 @@
----
-name: apple-container
-description: Use when running Apple's container tool on macOS - covers build, run, exec, and image management commands
----
-
-# Apple Container Tool
-
-macOS container runtime. Not Docker - different syntax for mounts and some flags.
-
-## First-Time Setup
-
-```bash
-container system start   # Required before first use, installs kernel
-```
-
-## Quick Reference
-
-| Task | Command |
-|------|---------|
-| Build image | `container build -t name:tag dir/` |
-| Run interactive | `container run -it image` |
-| Run with name | `container run --name mycontainer image` |
-| Run detached | `container run -d --name mycontainer image` |
-| List running | `container list` |
-| List all | `container list -a` |
-| Start stopped | `container start -a container-id` |
-| Stop | `container stop container-id` |
-| Remove | `container rm container-id` |
-| Exec into | `container exec -it container-id bash` |
-| Inspect | `container inspect container-id` |
-
-## Mount Syntax
-
-**Different from Docker.** Uses `--mount` with explicit format:
-
-```bash
-# Bind mount (host path to container path)
---mount type=bind,src=/host/path,dst=/container/path
-
-# Read-only bind mount
---mount type=bind,src=/host/path,dst=/container/path,readonly
-
-# tmpfs
---tmpfs /path
-```
-
-**Not supported:** Docker's `-v /host:/container` shorthand.
-
-## Common Patterns
-
-```bash
-# Build and run interactively
-container build -t myimage:latest . && container run -it --rm myimage:latest
-
-# Run with project mounted
-container run -it --rm \
-  --workdir /project \
-  --mount type=bind,src=$(pwd),dst=/project \
-  myimage:latest
-
-# Named container (persists after exit)
-container run -it --name mycontainer myimage:latest
-# Later: container start -ai mycontainer
-
-# Check if container exists (inspect returns [] for missing, not error)
-[ "$(container inspect mycontainer 2>/dev/null)" != "[]" ] && echo "exists"
-```
-
-## Image Management
-
-```bash
-container image list              # List local images
-container image rm image:tag      # Remove image
-container image prune             # Remove dangling images
-container image prune -a          # Remove all unused images
-```
-
-## Gotchas
-
-- **No `-a` for list by default** - stopped containers hidden without `container list -a`
-- **Mount syntax** - must use `--mount type=bind,src=...,dst=...` not `-v`
-- **System start required** - run `container system start` before first use
-- **Names are IDs** - `--name` sets the container ID, not a separate alias
-- **Inspect returns `[]` for missing** - doesn't error like Docker, check output not exit code
diff --git a/ai/skills/buildkite/SKILL.md b/ai/skills/buildkite/SKILL.md
deleted file mode 100644
index 105d4ba0..00000000
--- a/ai/skills/buildkite/SKILL.md
+++ /dev/null
@@ -1,71 +0,0 @@
----
-name: buildkite
-description: Use when needing bk CLI command reference, syntax, or behavior - directs to bk --help for authoritative documentation and flags
----
-
-# Buildkite CLI (bk)
-
-Use `bk --help` for authoritative command reference and current flags.
-
-## Quick Start
-
-Run `bk <command> --help` for detailed command documentation:
-
-```bash
-bk --help              # Full command list
-bk build view --help   # Specific command details
-bk job log --help      # Syntax and options
-```
-
-## Core Workflows
-
-**View current build:**
-```bash
-bk build view          # Current branch's latest build
-bk build watch         # Watch build progress in real-time
-```
-
-**Work with jobs:**
-```bash
-bk job list            # List jobs in current build
-bk job log <job-id>    # Get job logs
-bk job retry <job-id>  # Retry a failed job
-```
-
-**Create builds:**
-```bash
-bk build create        # Trigger new build for current branch
-bk build rebuild       # Rebuild a previous build
-```
-
-## Command Categories
-
-Run `bk --help` to see all commands. Main categories:
-
-Builds: build view, build list, build create, build watch, build cancel, build rebuild
-Jobs: job list, job log, job retry, job cancel, job unblock
-Pipelines: pipeline list, pipeline view, pipeline validate, pipeline create
-Artifacts: artifacts list, artifacts download
-Agents: agent list, agent view, agent pause, agent resume, agent stop
-Config: config list, config get, config set, configure add
-API: api (direct API calls)
-
-## Global Flags
-
-```bash
--y, --yes        # Skip confirmation prompts
---no-input       # Disable interactive prompts
--q, --quiet      # Suppress progress output
---debug          # Enable debug output for API calls
-```
-
-## When to Use bk --help
-
-Direct command help is always more current than documentation. Use it for:
-
-- Exact flag names and syntax
-- Current command options
-- Command behavior details
-- Examples and edge cases
-
-Never rely on this skill's command list as authoritative. Always run `bk <command> --help` to verify behavior, flags, and current options.
diff --git a/ai/skills/dump-session/SKILL.md b/ai/skills/dump-session/SKILL.md
deleted file mode 100644
index f31a5f36..00000000
--- a/ai/skills/dump-session/SKILL.md
+++ /dev/null
@@ -1,137 +0,0 @@
----
-name: dump-session
-description: This skill should be used when the user asks to "dump session", "export session", "save session", "/dump-session", "export conversation", "save this conversation", or "write session to markdown". Exports the current Claude Code session as Markdown and HTML files with a summary.
-disable-model-invocation: true
----
-
-# Dump Session
-
-Export the current Claude Code session as Markdown and HTML files in the
-repository, with a generated summary at the top and the full conversation below.
-
-## Workflow
-
-1. Identify the current session JSONL file
-2. Run the Markdown export script
-3. Run the HTML export script
-4. Read the exports and write summaries to replace the placeholders
-5. Review the exports for sensitive content and propose redactions
-6. Save both files to the repository
-
-## Identifying the Session
-
-The user may specify which session to export by passing a session UUID or a file
-path as an argument. When provided, use that value directly and skip
-auto-detection.
-
-When no argument is given, find the current session automatically. Claude Code
-stores sessions as JSONL files under `~/.claude/projects/`. The project
-directory name is the working directory path with slashes and dots replaced by
-dashes, prefixed with a dash:
-
-```
-~/.claude/projects/-Users-alpha-chen-src-project-name/
-```
-
-To find the current session file, identify the most recently modified `.jsonl`
-in the project directory. Run:
-
-```bash
-ls -t ~/.claude/projects/<project-dir>/*.jsonl | head -1
-```
-
-Derive `<project-dir>` from the current working directory by replacing `/` and
-`.` with `-`.
-
-## Running the Export
-
-Use the bundled script to convert the JSONL to Markdown:
-
-```bash
-~/.claude/skills/dump-session/scripts/dump-session.sh <session.jsonl|session-id> <output.md>
-```
-
-The first argument accepts either a path to a JSONL file or a session UUID. When
-given a UUID, the script searches `~/.claude/projects/` for a matching file.
-
-The script produces a Markdown document designed for GitHub PR descriptions:
-
-- A summary placeholder at the top (visible without expanding)
-- The full conversation in a collapsible `<details>` block
-- Session metadata (IDs, timestamps, tools) in a second collapsible block
-
-## Writing the Summary
-
-After the script runs, read the output file and replace the summary placeholder
-with a concise summary of the session. The summary should cover what was
-discussed, what was built or changed, and key decisions made. Write it in first
-person to match the user's voice.
-
-Replace:
-
-```markdown
-<!-- Replace this with a summary of the session -->
-```
-
-With the actual summary text.
-
-## Redaction Review
-
-After writing the summary, scan the full export for sensitive content that
-should not be shared. Present each finding to the user with `AskUserQuestion`
-and let them decide what to redact.
-
-Common categories to flag:
-
-- API keys, tokens, passwords, and credentials
-- `<system-reminder>` content that leaked into visible messages (instructions,
-  skill listings, CLAUDE.md contents)
-- Absolute filesystem paths that reveal machine or user directory structure
-- Internal hostnames, IP addresses, or URLs not meant to be public
-- Personal information (emails, phone numbers, employee IDs) that appeared in
-  tool output or conversation
-
-For each finding, show the surrounding context and propose a replacement such as
-`[REDACTED]` or a descriptive placeholder like `[API_KEY]`. Apply only the
-redactions the user approves.
-
-If nothing sensitive is found, tell the user and move on.
-
-## HTML Generation
-
-After the Markdown file is finalized (summary written, redactions applied),
-generate a self-contained HTML thread view from the same session:
-
-```bash
-~/.claude/skills/dump-session/scripts/dump-session-html.sh <session.jsonl|session-id> <output.html>
-```
-
-The HTML script produces a chat-style page with user and assistant messages,
-collapsed tool calls, collapsed skill content, metadata header, and a summary
-placeholder. It uses pandoc to render markdown within messages. The output
-includes inline CSS with dark/light mode support.
-
-After generating, replace the HTML summary placeholder the same way as the
-Markdown file. The summary `<div>` contains an HTML comment to replace.
-
-## Output Location
-
-Save both files to `docs/sessions/` in the repository root, creating the
-directory if needed. Name them with the date and a slug derived from the session
-topic:
-
-```
-docs/sessions/YYYY-MM-DD-topic-slug.md
-docs/sessions/YYYY-MM-DD-topic-slug.html
-```
-
-## Scripts
-
-- `scripts/parse-session.sh` — Parses a session JSONL file into structured
-  JSON (metadata, tool counts, filtered messages). Both formatters depend on
-  this script. Requires `jq` 1.6+.
-- `scripts/dump-session.sh` — Converts the intermediate JSON from
-  `parse-session.sh` into Markdown. Requires `jq` 1.6+.
-- `scripts/dump-session-html.sh` — Converts the intermediate JSON from
-  `parse-session.sh` into a self-contained HTML thread view. Requires `jq`
-  1.6+ and `pandoc`.
diff --git a/ai/skills/dump-session/scripts/dump-session-html.sh b/ai/skills/dump-session/scripts/dump-session-html.sh
deleted file mode 100755
index 8aed1df1..00000000
--- a/ai/skills/dump-session/scripts/dump-session-html.sh
+++ /dev/null
@@ -1,370 +0,0 @@
-#!/usr/bin/env bash
-#
-# dump-session-html.sh — Export a Claude Code session JSONL to a self-contained HTML file.
-#
-# Usage:
-#   dump-session-html.sh <session.jsonl|session-id> [output.html]
-#
-# The first argument can be a path to a JSONL file or a session UUID.
-# When given a UUID, the script searches ~/.claude/projects/ for a matching file.
-# If output.html is omitted, writes to stdout.
-#
-# Requires: jq 1.6+, pandoc
-
-set -euo pipefail
-
-if [[ $# -lt 1 ]]; then
-  echo "Usage: dump-session-html.sh <session.jsonl|session-id> [output.html]" >&2
-  exit 1
-fi
-
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-OUTPUT="${2:-/dev/stdout}"
-
-# Parse session JSONL into structured JSON.
-JSON=$("$SCRIPT_DIR/parse-session.sh" "$1")
-
-# Convert markdown content to HTML fragments via pandoc.
-# Iterates over each message, converts content, and reassembles.
-MESSAGES_HTML=$(echo "$JSON" | jq -r '.messages[] | @base64' | while read -r encoded; do
-  msg=$(echo "$encoded" | base64 --decode)
-  role=$(echo "$msg" | jq -r '.role')
-  timestamp=$(echo "$msg" | jq -r '.timestamp')
-  content=$(echo "$msg" | jq -r '.content')
-  tools=$(echo "$msg" | jq -c '.tools')
-  kind=$(echo "$msg" | jq -r '.kind // "message"')
-
-  # Convert markdown content to HTML
-  if [[ -n "$content" ]]; then
-    content_html=$(echo "$content" | pandoc -f markdown+raw_html -t html)
-  else
-    content_html=""
-  fi
-
-  # Build tool call HTML
-  tools_html=""
-  tool_count=$(echo "$tools" | jq 'length')
-  if [[ "$tool_count" -gt 0 ]]; then
-    tools_html=$(echo "$tools" | jq -r '.[] |
-      "<div class=\"tool-call\">" +
-      "<span class=\"tool-name\">" + .name + "</span>" +
-      (if .detail then " <code>" + (.detail | gsub("<"; "&lt;") | gsub(">"; "&gt;")) + "</code>" else "" end) +
-      "</div>"
-    ')
-  fi
-
-  # Skip empty assistant messages
-  if [[ "$role" == "assistant" && -z "$content_html" && "$tool_count" -eq 0 ]]; then
-    continue
-  fi
-
-  # Emit the message HTML
-  if [[ "$role" == "user" && "$kind" == "skill" ]]; then
-    # Extract skill name from the first H1 in the content
-    skill_name=$(echo "$content" | grep -m1 '^# ' | sed 's/^# //')
-    : "${skill_name:=Skill Content}"
-    cat <<MSGEOF
-<div class="message message-skill">
-  <details class="skill-content">
-    <summary>Skill loaded: ${skill_name}</summary>
-    <div class="skill-body"><p><em>Skill content omitted</em></p></div>
-  </details>
-</div>
-MSGEOF
-  elif [[ "$role" == "user" ]]; then
-    cat <<MSGEOF
-<div class="message message-user">
-  <div class="avatar">U</div>
-  <div class="message-content">
-    ${content_html}
-  </div>
-</div>
-MSGEOF
-  else
-    cat <<MSGEOF
-<div class="message message-assistant">
-MSGEOF
-    if [[ -n "$content_html" ]]; then
-      cat <<MSGEOF
-  <div class="assistant-text">${content_html}</div>
-MSGEOF
-    fi
-    if [[ "$tool_count" -gt 0 ]]; then
-      cat <<MSGEOF
-  <details class="tool-calls">
-    <summary>Tool calls (${tool_count})</summary>
-    ${tools_html}
-  </details>
-MSGEOF
-    fi
-    cat <<MSGEOF
-</div>
-MSGEOF
-  fi
-done)
-
-# Extract metadata for the header
-META_SESSION_ID=$(echo "$JSON" | jq -r '.meta.session_id')
-META_VERSION=$(echo "$JSON" | jq -r '.meta.version')
-META_CWD=$(echo "$JSON" | jq -r '.meta.cwd')
-META_BRANCH=$(echo "$JSON" | jq -r '.meta.branch')
-META_STARTED=$(echo "$JSON" | jq -r '.meta.started')
-META_ENDED=$(echo "$JSON" | jq -r '.meta.ended')
-META_USER_COUNT=$(echo "$JSON" | jq -r '.meta.user_count')
-META_ASSISTANT_COUNT=$(echo "$JSON" | jq -r '.meta.assistant_count')
-META_MODELS=$(echo "$JSON" | jq -r '.meta.models')
-TOOLS_HTML=$(echo "$JSON" | jq -r '[.tools[] | "\(.name) (\(.count))"] | join(" · ")')
-
-# Assemble the full HTML document
-cat <<'HTMLEOF' > "$OUTPUT"
-<!DOCTYPE html>
-<html lang="en">
-<head>
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Session Export</title>
-<style>
-:root {
-  color-scheme: dark light;
-  --bg: #0c0c0c;
-  --fg: #d4d4d4;
-  --fg-muted: #737373;
-  --border: #262626;
-  --surface: #171717;
-  --code-bg: #1a1a1a;
-  --user-border: #363636;
-  --radius: 8px;
-  --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Monaco, Consolas, monospace;
-}
-@media (prefers-color-scheme: light) {
-  :root {
-    --bg: #fff;
-    --fg: #1a1a1a;
-    --fg-muted: #737373;
-    --border: #e5e5e5;
-    --surface: #f7f7f7;
-    --code-bg: #f3f3f3;
-    --user-border: #d4d4d4;
-  }
-}
-*, *::before, *::after { box-sizing: border-box; }
-body {
-  margin: 0;
-  padding: 0;
-  background: var(--bg);
-  color: var(--fg);
-  font-family: var(--font);
-  font-size: 15px;
-  line-height: 1.6;
-  -webkit-font-smoothing: antialiased;
-}
-
-/* --- Layout --- */
-.page {
-  display: grid;
-  grid-template-columns: 260px 1fr;
-  min-height: 100vh;
-}
-@media (max-width: 860px) {
-  .page { grid-template-columns: 1fr; }
-  .sidebar {
-    position: static !important;
-    height: auto !important;
-    border-right: none !important;
-    border-bottom: 1px solid var(--border);
-  }
-}
-.sidebar {
-  position: sticky;
-  top: 0;
-  height: 100vh;
-  overflow-y: auto;
-  border-right: 1px solid var(--border);
-  padding: 1.5rem 1.25rem;
-  font-size: 0.8rem;
-  color: var(--fg-muted);
-}
-.conversation { max-width: 720px; padding: 2rem 1.5rem; }
-
-/* --- Sidebar metadata --- */
-.session-meta { margin: 0; padding: 0; }
-.session-meta dt { font-weight: 600; margin-top: 0.6rem; }
-.session-meta dt:first-child { margin-top: 0; }
-.session-meta dd { margin: 0; word-break: break-all; }
-.session-tools { margin-top: 0.75rem; }
-.session-summary {
-  margin-top: 1.25rem;
-  padding-top: 1.25rem;
-  border-top: 1px solid var(--border);
-}
-.session-summary h2 { margin: 0 0 0.5rem; font-size: 0.85rem; font-weight: 600; color: var(--fg); }
-
-/* --- Messages --- */
-.message { margin: 1.25rem 0; }
-.message-user {
-  display: grid;
-  grid-template-columns: 28px 1fr;
-  gap: 0.5rem;
-  align-items: start;
-}
-.avatar {
-  width: 28px;
-  height: 28px;
-  border-radius: 50%;
-  background: var(--border);
-  color: var(--fg-muted);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 0.7rem;
-  font-weight: 600;
-  flex-shrink: 0;
-}
-.message-content {
-  border: 1px solid var(--user-border);
-  border-radius: var(--radius);
-  padding: 0.5rem 0.75rem;
-  min-width: 0;
-}
-.message-content p:first-child { margin-top: 0; }
-.message-content p:last-child { margin-bottom: 0; }
-.message-assistant { padding-left: 36px; }
-.assistant-text p:first-child { margin-top: 0; }
-.assistant-text p:last-child { margin-bottom: 0; }
-
-/* --- Tool calls --- */
-.tool-calls {
-  margin: 0.75rem 0;
-  border: 1px solid var(--border);
-  border-radius: var(--radius);
-  font-size: 0.85rem;
-}
-.tool-calls summary {
-  cursor: pointer;
-  padding: 0.375rem 0.75rem;
-  color: var(--fg-muted);
-  user-select: none;
-}
-.tool-calls summary:hover { color: var(--fg); }
-.tool-calls[open] summary { border-bottom: 1px solid var(--border); }
-.tool-call {
-  padding: 0.25rem 0.75rem;
-  border-bottom: 1px solid var(--border);
-}
-.tool-call:last-child { border-bottom: none; }
-.tool-name {
-  font-weight: 600;
-  font-size: 0.8rem;
-}
-.tool-call code {
-  font-family: var(--mono);
-  font-size: 0.8rem;
-  color: var(--fg-muted);
-}
-
-/* --- Skill content (collapsed) --- */
-.message-skill { padding-left: 36px; }
-.skill-content {
-  border: 1px solid var(--border);
-  border-radius: var(--radius);
-  font-size: 0.85rem;
-}
-.skill-content summary {
-  cursor: pointer;
-  padding: 0.375rem 0.75rem;
-  color: var(--fg-muted);
-  font-style: italic;
-  user-select: none;
-}
-.skill-content summary:hover { color: var(--fg); }
-.skill-content[open] summary { border-bottom: 1px solid var(--border); }
-.skill-body {
-  padding: 0.5rem 0.75rem;
-  max-height: 300px;
-  overflow-y: auto;
-  color: var(--fg-muted);
-}
-
-/* --- Markdown content --- */
-pre {
-  background: var(--code-bg);
-  border-radius: 6px;
-  padding: 0.75rem 1rem;
-  overflow-x: auto;
-  font-size: 0.85rem;
-  line-height: 1.5;
-}
-code {
-  font-family: var(--mono);
-  font-size: 0.9em;
-}
-p code {
-  background: var(--code-bg);
-  padding: 0.15em 0.35em;
-  border-radius: 4px;
-}
-pre code { background: none; padding: 0; font-size: inherit; }
-a { color: #60a5fa; text-decoration: underline; text-underline-offset: 2px; }
-a:hover { text-decoration: none; }
-@media (prefers-color-scheme: light) {
-  a { color: #2563eb; }
-}
-ul, ol { padding-left: 1.5rem; margin: 0.5em 0; }
-li { margin: 0.25em 0; }
-table { border-collapse: collapse; width: 100%; margin: 0.75em 0; font-size: 0.9em; }
-th, td { border: 1px solid var(--border); padding: 0.375rem 0.5rem; text-align: left; }
-th { background: var(--surface); font-weight: 600; }
-blockquote {
-  border-left: 3px solid var(--border);
-  margin: 0.75em 0;
-  padding: 0.25em 1em;
-  color: var(--fg-muted);
-}
-h1, h2, h3, h4, h5, h6 { line-height: 1.3; }
-h1 { font-size: 1.5rem; }
-h2 { font-size: 1.25rem; }
-h3 { font-size: 1.1rem; }
-strong { font-weight: 600; }
-</style>
-</head>
-<body>
-<div class="page">
-HTMLEOF
-
-# Write sidebar
-cat <<HEADEREOF >> "$OUTPUT"
-<aside class="sidebar">
-  <dl class="session-meta">
-    <dt>Session</dt><dd><code>${META_SESSION_ID}</code></dd>
-    <dt>Claude Code</dt><dd>v${META_VERSION}</dd>
-    <dt>Directory</dt><dd><code>${META_CWD}</code></dd>
-    <dt>Branch</dt><dd>${META_BRANCH}</dd>
-    <dt>Started</dt><dd>${META_STARTED}</dd>
-    <dt>Ended</dt><dd>${META_ENDED}</dd>
-    <dt>Messages</dt><dd>${META_USER_COUNT} user · ${META_ASSISTANT_COUNT} assistant</dd>
-    <dt>Models</dt><dd>${META_MODELS}</dd>
-  </dl>
-  <div class="session-tools">Tools: ${TOOLS_HTML}</div>
-  <div class="session-summary">
-    <h2>Summary</h2>
-    <!-- Replace this with a summary of the session -->
-  </div>
-</aside>
-<main class="conversation">
-HEADEREOF
-
-# Write messages
-echo "$MESSAGES_HTML" >> "$OUTPUT"
-
-# Close HTML
-cat <<'HTMLEOF' >> "$OUTPUT"
-</main>
-</div>
-</body>
-</html>
-HTMLEOF
-
-if [[ "$OUTPUT" != "/dev/stdout" ]]; then
-  echo "Exported to $OUTPUT" >&2
-fi
diff --git a/ai/skills/dump-session/scripts/dump-session.sh b/ai/skills/dump-session/scripts/dump-session.sh
deleted file mode 100755
index 1da71e90..00000000
--- a/ai/skills/dump-session/scripts/dump-session.sh
+++ /dev/null
@@ -1,76 +0,0 @@
-#!/usr/bin/env bash
-#
-# dump-session.sh — Export a Claude Code session JSONL to readable Markdown.
-#
-# Usage:
-#   dump-session.sh <session.jsonl|session-id> [output.md]
-#
-# The first argument can be a path to a JSONL file or a session UUID.
-# When given a UUID, the script searches ~/.claude/projects/ for a matching file.
-# If output.md is omitted, writes to stdout.
-
-set -euo pipefail
-
-if [[ $# -lt 1 ]]; then
-  echo "Usage: dump-session.sh <session.jsonl|session-id> [output.md]" >&2
-  exit 1
-fi
-
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-OUTPUT="${2:-/dev/stdout}"
-
-# Parse session JSONL into structured JSON, then format as Markdown.
-"$SCRIPT_DIR/parse-session.sh" "$1" | jq -r '
-  # Format conversation
-  (.messages | map(
-    if .role == "user" and .kind == "skill" then
-      (.content | split("\n") | map(select(startswith("# ")))[0] // "Skill Content" |
-        ltrimstr("# ")) as $skill_name |
-      "<details><summary>Skill loaded: \($skill_name)</summary>\n\n[Skill content omitted]\n\n</details>\n"
-    elif .role == "user" then
-      "### User <sub>\(.timestamp)</sub>\n\n\(.content)\n"
-    elif .role == "assistant" then
-      ([.tools[] |
-        "- **\(.name)**" +
-        if .detail then " `\(.detail)`"
-        else ""
-        end
-      ]) as $tool_lines |
-
-      "### Assistant <sub>\(.timestamp)</sub>\n\n" +
-      (if (.content | length) > 0 then .content + "\n\n" else "" end) +
-      (if ($tool_lines | length) > 0 then
-        "<details><summary>Tool calls</summary>\n\n" +
-        ($tool_lines | join("\n")) +
-        "\n\n</details>\n"
-      else "" end)
-    else ""
-    end
-  ) | map(select(. != "")) | join("\n")) as $conversation |
-
-  # Format tool list
-  (.tools | map("\(.name) (\(.count))") | join(" · ")) as $tool_list |
-
-  # Assemble document — designed for GitHub PR descriptions.
-  # Summary first (visible without expanding), conversation and metadata collapsed.
-  "<!-- Replace this with a summary of the session -->\n\n" +
-  "<details><summary>Session transcript</summary>\n\n" +
-  $conversation + "\n\n" +
-  "</details>\n\n" +
-  "<details><summary>Session metadata</summary>\n\n" +
-  "| Field | Value |\n|-------|-------|\n" +
-  "| Session ID | `\(.meta.session_id)` |\n" +
-  "| Claude Code | v\(.meta.version) |\n" +
-  "| Directory | `\(.meta.cwd)` |\n" +
-  "| Branch | `\(.meta.branch)` |\n" +
-  "| Started | \(.meta.started) |\n" +
-  "| Ended | \(.meta.ended) |\n" +
-  "| Messages | \(.meta.user_count) user · \(.meta.assistant_count) assistant |\n" +
-  "| Models | \(.meta.models) |\n" +
-  "| Tools | \($tool_list) |\n\n" +
-  "</details>\n"
-' > "$OUTPUT"
-
-if [[ "$OUTPUT" != "/dev/stdout" ]]; then
-  echo "Exported to $OUTPUT" >&2
-fi
diff --git a/ai/skills/dump-session/scripts/parse-session.sh b/ai/skills/dump-session/scripts/parse-session.sh
deleted file mode 100755
index 10ae5edd..00000000
--- a/ai/skills/dump-session/scripts/parse-session.sh
+++ /dev/null
@@ -1,126 +0,0 @@
-#!/usr/bin/env bash
-#
-# parse-session.sh — Parse a Claude Code session JSONL into structured JSON.
-#
-# Usage:
-#   parse-session.sh <session.jsonl|session-id> [output.json]
-#
-# The first argument can be a path to a JSONL file or a session UUID.
-# When given a UUID, the script searches ~/.claude/projects/ for a matching file.
-# If output.json is omitted, writes to stdout.
-#
-# Requires: jq 1.6+
-
-set -euo pipefail
-
-if [[ $# -lt 1 ]]; then
-  echo "Usage: parse-session.sh <session.jsonl|session-id> [output.json]" >&2
-  exit 1
-fi
-
-INPUT="$1"
-OUTPUT="${2:-/dev/stdout}"
-
-# Resolve input: file path or session UUID
-if [[ -f "$INPUT" ]]; then
-  SESSION_FILE="$INPUT"
-elif [[ "$INPUT" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
-  SESSION_FILE=$(find ~/.claude/projects -name "${INPUT}.jsonl" -print -quit 2>/dev/null)
-  if [[ -z "$SESSION_FILE" ]]; then
-    echo "Error: No session file found for ID: $INPUT" >&2
-    exit 1
-  fi
-else
-  echo "Error: Not a file path or valid session UUID: $INPUT" >&2
-  exit 1
-fi
-
-jq -rs '
-  (map(select(.type == "user"))) as $users |
-  (map(select(.type == "assistant"))) as $assistants |
-  (map(select(.type == "user" or .type == "assistant"))) as $messages |
-
-  ($users[0] // {}) as $first |
-
-  # Tool usage counts
-  ([
-    $assistants[].message.content[]? |
-    select(type == "object" and .type == "tool_use") | .name
-  ] | group_by(.) | map({name: .[0], count: length}) | sort_by(-.count)) as $tools |
-
-  # Build messages array
-  ($messages | map(
-    if .type == "user" then
-      (.message.content |
-        if type == "string" then .
-        elif type == "array" then
-          [.[] |
-            if type == "string" then .
-            elif .type == "text" then .text
-            elif .type == "tool_result" then null
-            else null
-            end
-          ] | map(select(. != null)) | join("\n\n")
-        else ""
-        end
-      ) as $content |
-
-      (.message.content |
-        if type == "array" then all(type == "object" and .type == "tool_result")
-        else false
-        end
-      ) as $is_tool_result_only |
-
-      ($content | test("^<(command-name|local-command|system-reminder)")) as $is_system |
-
-      ($content | test("^Base directory for this skill:")) as $is_skill |
-
-      if $is_tool_result_only or $is_system or ($content | length) == 0 then
-        null
-      else
-        {role: "user", timestamp: (.timestamp // ""), content: $content, tools: [],
-         kind: (if $is_skill then "skill" else "message" end)}
-      end
-
-    elif .type == "assistant" then
-      ([.message.content[]? | select(type == "object" and .type == "text") | .text] | join("\n\n")) as $text |
-
-      ([.message.content[]? | select(type == "object" and .type == "tool_use") |
-        {
-          name: .name,
-          detail: (
-            if .input.command then (.input.command | tostring | split("\n") | .[0])
-            elif .input.pattern then .input.pattern
-            elif .input.file_path then .input.file_path
-            else null
-            end
-          )
-        }
-      ]) as $tool_list |
-
-      if ($text | length) == 0 and ($tool_list | length) == 0 then
-        null
-      else
-        {role: "assistant", timestamp: (.timestamp // ""), content: $text, tools: $tool_list,
-         kind: "message"}
-      end
-    else null
-    end
-  ) | map(select(. != null))) as $msg_list |
-
-  {
-    meta: {
-      session_id: ($first.sessionId // "unknown"),
-      version: ($first.version // "unknown"),
-      cwd: ($first.cwd // "unknown"),
-      branch: ($first.gitBranch // "unknown"),
-      started: ($messages | first | .timestamp // ""),
-      ended: ($messages | last | .timestamp // ""),
-      user_count: ($users | length),
-      assistant_count: ($assistants | length),
-      models: ([$assistants[].message.model // empty] | unique | join(", "))
-    },
-    tools: $tools,
-    messages: $msg_list
-  }
-' "$SESSION_FILE" > "$OUTPUT"
diff --git a/ai/skills/gitea/SKILL.md b/ai/skills/gitea/SKILL.md
deleted file mode 100644
index 9270a4df..00000000
--- a/ai/skills/gitea/SKILL.md
+++ /dev/null
@@ -1,185 +0,0 @@
----
-name: gitea
-description: Use when needing tea CLI command reference, syntax, or behavior - directs to tea --help for authoritative documentation and flags
----
-
-# Gitea CLI (tea)
-
-Use `tea --help` for authoritative command reference and current flags.
-
-## Quick Start
-
-Run `tea <command> --help` for detailed command documentation:
-
-```bash
-tea --help              # Full command list
-tea issues --help       # Specific command details
-tea pulls create --help # Syntax and options
-```
-
-tea auto-discovers the Gitea instance and repo from the current directory's git remote.
-
-## Targeting Remote Repos
-
-When not inside a clone of the target repo, specify the repo and login explicitly:
-
-```bash
-tea issues list -r owner/repo -l instance-name
-tea issues create -r owner/repo -l instance-name -t "Title" -d "Body"
-```
-
-`-l` matches the NAME column from `tea logins list`. Use `-r owner/repo` (the slug, not a URL).
-These flags are needed on **every command** when the cwd isn't a clone with a matching remote.
-
-## Core Workflows
-
-**Issues:**
-```bash
-tea issues                      # List open issues
-tea issues <index>              # Show issue details
-tea issues create -t "Title" -d "Description"  # Create (inline)
-tea issues create               # Create (interactive)
-tea issues edit <index>         # Edit an issue
-tea issues edit <idx> [<idx>...] --add-labels "a,b"  # Bulk label
-tea issues close <index>        # Close an issue
-tea comment <index> "<body>"    # Comment on an issue or PR
-```
-
-**Pull requests:**
-```bash
-tea pulls                       # List open PRs
-tea pulls <index>               # Show PR details
-tea pulls create                # Create a PR
-tea pulls checkout <index>      # Check out a PR locally
-tea pulls merge <index>         # Merge a PR
-tea pulls approve <index>       # Approve a PR
-tea pulls reject <index>        # Request changes
-tea pulls review <index>        # Interactive review
-```
-
-**Labels & milestones:**
-```bash
-tea labels list                 # List labels
-tea labels create --name "bug" --color "e11d48" --description "Bug reports"
-tea milestones list             # List milestones
-tea milestones create           # Create a milestone
-```
-
-**Repositories:**
-```bash
-tea repos                       # Show current repo details
-tea repos list                  # List your repos
-tea repos search <query>        # Search repos on the instance
-tea repos create                # Create a repo
-tea repos fork                  # Fork a repo
-```
-
-**Releases:**
-```bash
-tea releases list               # List releases
-tea releases create             # Create a release
-```
-
-## Common Patterns
-
-### Batch issue creation with labels
-
-Create labels first, then apply on create or after:
-
-```bash
-# 1. Create labels
-tea labels create -r o/r -l inst --name "security" --color "e11d48"
-tea labels create -r o/r -l inst --name "component" --color "0075ca"
-
-# 2a. Create issues with labels inline
-tea issues create -r o/r -l inst -t "Title" -d "Body" -L "security,component"
-
-# 2b. Or add labels to existing issues (supports multiple indices)
-tea issues edit -r o/r -l inst --add-labels "security,component" 1 2 3
-```
-
-### Bulk labeling in a loop
-
-```bash
-for i in 1 2 3 4 5; do
-  tea issues edit -r o/r -l inst --add-labels "label1,label2" "$i"
-done
-```
-
-### Issue descriptions with markdown
-
-Pass multiline markdown via `-d` with shell quoting:
-
-```bash
-tea issues create -r o/r -l inst \
-  -t '[CRITICAL] Issue title' \
-  -d '## Summary
-
-Description with **markdown** and code:
-
-```typescript
-const x = 1;
-```
-
-## Impact
-
-Bullet points:
-- First
-- Second'
-```
-
-## Gotchas
-
-- **No project board support:** tea cannot add issues to Gitea project boards. Use the web UI or Gitea API directly.
-- **Labels flag differs between create and edit:** `issues create` uses `-L`/`--labels`, while `issues edit` uses `--add-labels`/`--remove-labels`.
-- **`-l` is `--login`, not `--labels`:** The `-L` (uppercase) shorthand is for labels. Mixing these up silently targets the wrong Gitea instance.
-
-## Command Categories
-
-Run `tea --help` to see all commands. Main categories:
-
-Issues: issues list/create/edit/close/reopen
-Pull Requests: pulls list/create/checkout/close/merge/approve/reject/review/clean
-Comments: comment (on issues or PRs)
-Repos: repos list/search/create/fork/migrate/delete
-Labels: labels list/create/update/delete
-Milestones: milestones list/create
-Releases: releases list/create/edit/delete, releases assets
-Times: times (tracked time on issues/PRs)
-Organizations: organizations list/create/delete
-Helpers: open (open in browser), notifications, clone
-Setup: logins, logout, whoami
-
-## Output Formats
-
-All listing commands support `--output` / `-o`:
-
-```bash
-tea issues --output json        # JSON output (useful for scripting)
-tea issues --output yaml        # YAML output
-tea issues --output csv         # CSV output
-tea issues --output table       # Table output
-```
-
-## Filtering
-
-Issues and PRs support filtering:
-
-```bash
-tea issues --state closed           # By state (open/closed/all)
-tea issues --labels "bug,urgent"    # By labels
-tea issues --milestones "v1.0"      # By milestone
-tea issues --assignee "username"    # By assignee
-tea issues --keyword "search term"  # By keyword search
-```
-
-## When to Use tea --help
-
-Direct command help is always more current than documentation. Use it for:
-
-- Exact flag names and syntax
-- Current command options
-- Command behavior details
-- Examples and edge cases
-
-Never rely on this skill's command list as authoritative. Always run `tea <command> --help` to verify behavior, flags, and current options.
diff --git a/ai/skills/ruby-typing/SKILL.md b/ai/skills/ruby-typing/SKILL.md
deleted file mode 100644
index 2e742065..00000000
--- a/ai/skills/ruby-typing/SKILL.md
+++ /dev/null
@@ -1,81 +0,0 @@
----
-name: ruby-typing
-description: Use when adding type annotations to Ruby code, running Steep or rbs-inline, or troubleshooting type errors in Ruby projects
----
-
-# Ruby Type Checking with Steep and rbs-inline
-
-## Workflow
-
-1. Add `# rbs_inline: enabled` at the top of the file
-2. Annotate methods and Data.define fields with `#:` comments
-3. Run `rbs-inline --output lib/` to generate `.rbs` files under `sig/generated/`
-4. Run `steep check` to verify
-
-## rbs-inline Annotations
-
-Method signatures use `#:` on the line before `def`:
-
-```ruby
-#: (String, Integer) -> String
-def repeat(str, count)
-  str * count
-end
-
-#: () -> void
-def reset!
-  @count = 0
-end
-```
-
-Data.define fields are annotated inline per member:
-
-```ruby
-MyConfig = Data.define(
-  :name,     #: String
-  :timeout,  #: Integer?
-)
-```
-
-This generates typed `attr_reader`, `self.new`, and `members` in the RBS output.
-
-## Known Limitations
-
-rbs-inline ignores methods defined inside a `Data.define` block. Reopen the class in a separate body:
-
-```ruby
-# Won't work — rbs-inline can't see parse:
-MyData = Data.define(:name) do
-  def self.parse(value) = new(name: value)
-end
-
-# Works — reopen the class:
-MyData = Data.define(:name)
-class MyData
-  #: (String) -> MyData
-  def self.parse(value) = new(name: value)
-end
-```
-
-`String#split` returns `Array[String]`, so destructuring gives `String?` for each element. Use a tuple assertion to tell Steep the exact shape:
-
-```ruby
-login, name = value.split(":", 2) #: [String, String?]
-```
-
-## Steep Tips
-
-`Hash[String, String]` for env-like hashes. Use `String?` values only if the hash actually contains nils — `ENV#fetch` with a block returns `String`, not `String?`.
-
-Steep differentiates `instance` from the class name in return types. `Data.define` constructors return `instance`; your own class methods should return the class name (`MyData`, not `instance`).
-
-## Quick Reference
-
-| Annotation | Meaning |
-|---|---|
-| `#: (String) -> Integer` | Method type (before def) |
-| `#: String` | Field/attr type (inline after member) |
-| `#: [String, String?]` | Tuple type assertion (inline after expression) |
-| `# @rbs skip` | Exclude from RBS generation |
-| `# @rbs override` | Method overrides parent |
-| `# rbs_inline: enabled` | Opt file into RBS generation |