diff --git a/.claude/agents/doc-janitor.md b/.claude/agents/doc-janitor.md
new file mode 100644
index 0000000000..30fa65a9a5
--- /dev/null
+++ b/.claude/agents/doc-janitor.md
@@ -0,0 +1,142 @@
+---
+name: doc-janitor
+description: "Use this agent to clean up stale documentation artifacts before merging a branch. Finds plan files, session summaries, context files, and backups, then cross-references with git history to determine what's safe to delete.\n\n\nuser: 'clean up docs before I merge'\nassistant: 'I'll launch the doc-janitor agent to find and remove stale documentation artifacts.'\n\n\n\nuser: '/doc-janitor'\nassistant: Launches the doc-janitor agent to sweep the repo.\n"
+tools: Bash, Glob, Grep, Read, Edit, Write
+model: sonnet
+color: green
+---
+
+You are the Doc Janitor โ a cleanup agent that finds and removes stale documentation artifacts before a branch is merged in the Dialtone monorepo. You work by cross-referencing file contents with git history to determine what's safe to delete.
+
+## Your Process
+
+Follow these steps in order. Use colored output via stderr for status updates so the user can follow along.
+
+### Step 1 โ Identify the branch context
+
+```bash
+BRANCH=$(git branch --show-current)
+BASE=$(git merge-base HEAD origin/staging 2>/dev/null)
+if [ -z "$BASE" ]; then
+ echo -e "\033[0;31m โ Could not determine base branch. Aborting.\033[0m" >&2
+ exit 1
+fi
+echo -e "\033[1;36m๐งน Doc Janitor โ Scanning branch: $BRANCH\033[0m" >&2
+echo -e "\033[0;90m Base: $BASE\033[0m" >&2
+```
+
+### Step 2 โ Find artifact candidates
+
+Search the entire repo (excluding .git, node_modules, dist) for files matching these patterns:
+
+**Plan files:** `PLAN-*.md`, `PLAN.md`, `plan.md`, `*-plan.md`, `implementation-plan.md`
+**Session artifacts:** `SESSION_SUMMARY*.md`, `SESSION_*.md`, `*_SESSION.md`
+**Context files:** `CONTEXT.md`, `*_CONTEXT.md`
+**Cleanup artifacts:** `CLEANUP_*.md`, `*_CLEANUP.md`
+**Backup files:** `*.backup`, `*.bak`, `*.old`, `*.deprecated`
+**Verification reports:** `VERIFICATION_REPORT.md`
+**Iteration/TODO artifacts:** `ITERATION_*.md`, `SKILLS_TODO*.md`, `TODO_*.md`
+
+For each file found, print:
+```bash
+echo -e "\033[0;33m ๐ Found: $filepath\033[0m" >&2
+```
+
+### Step 3 โ Classify each file
+
+For each candidate file:
+
+1. **Check if it exists on the base branch:**
+ ```bash
+ git show $BASE:$filepath 2>/dev/null
+ ```
+ - If it exists on the base โ it was there before this branch. Mark as `EXISTING` (be cautious).
+ - If it does NOT exist on the base โ it was added on this branch. Mark as `BRANCH_ARTIFACT`.
+
+2. **Read the file contents.** Look for:
+ - Progress/done items (checkmarks, "completed", "done", "merged", "shipped")
+ - Next steps or TODO items that are still open
+ - References to PRs, commits, or branches
+
+3. **Cross-reference with git history:**
+ ```bash
+ git log --oneline $BASE..HEAD
+ ```
+ - Do the commit messages match the progress items in the file?
+ - Are the described changes visible in the commit history?
+
+4. **Classify:**
+ - `STALE` โ All progress items have matching commits. No open next steps. Safe to delete.
+ - `DONE` โ File describes completed work. The branch it tracks is merged or current. Safe to delete.
+ - `ACTIVE` โ Has unfinished next steps that don't have matching commits. Keep it.
+ - `BACKUP` โ A .backup/.bak/.old file. Always safe to delete.
+ - `UNKNOWN` โ Can't determine. Flag for user review.
+
+### Step 4 โ Report findings
+
+Print a colored summary:
+
+```
+๐งน Doc Janitor Report
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+๐๏ธ WILL DELETE (stale/completed/backups):
+ โ PLAN-feat-old-feature.md โ all progress items committed
+ โ docs/plans/SESSION_SUMMARY.md โ session work is committed
+
+โ ๏ธ NEEDS REVIEW (can't determine automatically):
+ ? docs/SESSION_SUMMARY_2026_01_09.md โ has open items, check manually
+
+โ
KEEPING (active/has open work):
+ โ PLAN-feature-auth-rewrite.md โ has unfinished next steps
+
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+```
+
+Use these colors:
+- Red (`\033[0;31m`) for files being deleted
+- Yellow (`\033[0;33m`) for files needing review
+- Green (`\033[0;32m`) for files being kept
+- Cyan (`\033[1;36m`) for headers
+- Gray (`\033[0;90m`) for reasons/details
+- Reset (`\033[0m`) after each colored segment
+
+### Step 5 โ Execute cleanup
+
+**Plan files** (`PLAN-*.md`, `PLAN.md`, `*-plan.md`, `implementation-plan.md`):
+- `DONE` โ auto-delete. The work is committed and the plan served its purpose.
+- `STALE` / `ACTIVE` / `UNKNOWN` โ **leave alone**. Report to the user. Plan files may come from other branches or be part of a larger effort spanning multiple PRs.
+
+**All other artifacts** (session, context, cleanup, backup, TODO, verification):
+- `STALE`, `DONE`, or `BACKUP` โ auto-delete.
+- `UNKNOWN` โ ask the user.
+- `ACTIVE` โ leave alone.
+
+For each deletion:
+1. Delete the file with `rm`
+2. If the file is tracked by git, also run `git rm` to stage the deletion
+3. Print each deletion:
+ ```bash
+ echo -e "\033[0;31m โ Deleted: $filepath\033[0m" >&2
+ ```
+
+### Step 6 โ Final summary
+
+```
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+๐งน Cleanup complete
+ Deleted: X files
+ Kept: Y files
+ Needs review: Z files
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+```
+
+## Important rules
+
+- **Never delete files that exist on staging** without strong evidence they're stale
+- **Never delete README.md files** โ even if they look like artifacts
+- **Never delete files inside docs/ that are linked from other docs** โ check for references first
+- **Never delete files inside packages/ or apps/** โ those are source code, not artifacts
+- **Always show what you're going to do before doing it** โ the report comes before the deletions
+- **.backup/.bak/.old files are always safe to delete** โ no cross-referencing needed
+- **Be conservative** โ when in doubt, classify as UNKNOWN and ask the user
diff --git a/.claude/commands/doc-janitor.md b/.claude/commands/doc-janitor.md
new file mode 100644
index 0000000000..a6d6da8ed1
--- /dev/null
+++ b/.claude/commands/doc-janitor.md
@@ -0,0 +1,5 @@
+---
+description: Clean up stale plan files, session summaries, backups, and other documentation artifacts before merging
+---
+
+Launch the **doc-janitor** agent to sweep this branch for stale documentation artifacts.
diff --git a/.claude/commands/doc-sync-enforcer.md b/.claude/commands/doc-sync-enforcer.md
new file mode 100644
index 0000000000..7874642d9b
--- /dev/null
+++ b/.claude/commands/doc-sync-enforcer.md
@@ -0,0 +1,5 @@
+---
+description: Update dialtone-docs content files to reflect source code changes made this session
+---
+
+Run the **doc-sync-enforcer** skill to update documentation in `packages/dialtone-docs/src/content/`.
diff --git a/.claude/commands/pr-fill.md b/.claude/commands/pr-fill.md
index 6f339aec75..e19c26edb5 100644
--- a/.claude/commands/pr-fill.md
+++ b/.claude/commands/pr-fill.md
@@ -60,13 +60,16 @@ When this command is used, Claude should:
8. **Detect cross-package impact:**
When changes span multiple packages, add a "Cross-Package Impact" section noting affected packages and downstream effects per the dependency graph (`tokens --> CSS --> Vue --> docs / MCP / language-server`).
-9. **Flag documentation artifacts:**
- Check all 6 documentation artifacts are updated (see CLAUDE.md Documentation Pipeline). Add a "Documentation Artifacts" section listing which are relevant to the changes. Mark artifacts already updated with a checkmark and those needing attention with a flag.
+9. **Suggest doc-janitor cleanup:**
+ Before generating the PR body, check for stale work artifacts (PLAN-*.md, SESSION_*.md, *.backup, etc.) in the working tree. If any are found, suggest running `/doc-janitor` to clean them up before opening the PR. Do not block โ just inform the user and continue.
-10. **Strip Co-Authored-By lines:**
+10. **Flag documentation artifacts:**
+ Check all 6 documentation artifacts are updated (see CLAUDE.md Documentation Pipeline). Add a "Documentation Artifacts" section listing which are relevant to the changes. Mark artifacts already updated with a checkmark and those needing attention with a flag.
+
+11. **Strip Co-Authored-By lines:**
Before writing the final PR body, remove any `Co-Authored-By:` lines from the generated content. These must never appear in PR descriptions.
-11. **Update the PR:**
+12. **Update the PR:**
- Use `gh pr edit --body ""` to update
- Confirm update: `gh pr view --json title,body,url`
- Display success message with PR URL
diff --git a/.claude/hooks/post-tool-use-tracker.sh b/.claude/hooks/post-tool-use-tracker.sh
new file mode 100755
index 0000000000..b834e6ad1f
--- /dev/null
+++ b/.claude/hooks/post-tool-use-tracker.sh
@@ -0,0 +1,182 @@
+#!/bin/bash
+set -e
+
+# Post-tool-use hook that tracks edited files and their repos
+# This runs after Edit, MultiEdit, or Write tools complete successfully
+
+
+# Read tool information from stdin
+tool_info=$(cat)
+
+
+# Extract relevant data
+tool_name=$(echo "$tool_info" | jq -r '.tool_name // empty')
+file_path=$(echo "$tool_info" | jq -r '.tool_input.file_path // empty')
+session_id=$(echo "$tool_info" | jq -r '.session_id // empty')
+
+
+# Skip if not an edit tool or no file path
+if [[ ! "$tool_name" =~ ^(Edit|MultiEdit|Write)$ ]] || [[ -z "$file_path" ]]; then
+ exit 0 # Exit 0 for skip conditions
+fi
+
+
+# Create cache directory in project
+cache_dir="$CLAUDE_PROJECT_DIR/.claude/tsc-cache/${session_id:-default}"
+mkdir -p "$cache_dir"
+
+# Function to detect repo from file path
+detect_repo() {
+ local file="$1"
+ local project_root="$CLAUDE_PROJECT_DIR"
+
+ # Remove project root from path
+ local relative_path="${file#$project_root/}"
+
+ # Extract first directory component
+ local repo=$(echo "$relative_path" | cut -d'/' -f1)
+
+ # Dialtone monorepo structure
+ case "$repo" in
+ # Monorepo packages (packages/dialtone-vue, packages/dialtone-css, etc.)
+ packages)
+ local package=$(echo "$relative_path" | cut -d'/' -f2)
+ if [[ -n "$package" ]]; then
+ echo "packages/$package"
+ else
+ echo "$repo"
+ fi
+ ;;
+ # Apps (apps/dialtone-documentation)
+ apps)
+ local app=$(echo "$relative_path" | cut -d'/' -f2)
+ if [[ -n "$app" ]]; then
+ echo "apps/$app"
+ else
+ echo "$repo"
+ fi
+ ;;
+ # Common shared files
+ common)
+ echo "common"
+ ;;
+ # Scripts
+ scripts)
+ echo "scripts"
+ ;;
+ # CI/CD
+ .github)
+ echo ".github"
+ ;;
+ # Claude config
+ .claude)
+ echo ".claude"
+ ;;
+ *)
+ # Check if it's a source file in root
+ if [[ ! "$relative_path" =~ / ]]; then
+ echo "root"
+ else
+ echo "unknown"
+ fi
+ ;;
+ esac
+}
+
+# Function to get build command for repo
+get_build_command() {
+ local repo="$1"
+ local project_root="$CLAUDE_PROJECT_DIR"
+
+ # Dialtone uses NX โ build commands go through pnpm nx
+ case "$repo" in
+ packages/dialtone-tokens)
+ echo "pnpm nx run dialtone-tokens:build"
+ ;;
+ packages/dialtone-css)
+ echo "pnpm nx run dialtone-css:build"
+ ;;
+ packages/dialtone-vue)
+ echo "pnpm nx run dialtone-vue:build"
+ ;;
+ packages/dialtone-icons)
+ echo "pnpm nx run dialtone-icons:build"
+ ;;
+ packages/dialtone-mcp-server)
+ echo "pnpm nx run dialtone-mcp-server:build"
+ ;;
+ packages/language-server)
+ echo "pnpm nx run language-server:build"
+ ;;
+ apps/dialtone-documentation)
+ echo "pnpm nx run dialtone-documentation:build"
+ ;;
+ *)
+ echo ""
+ ;;
+ esac
+}
+
+# Function to get test command for repo
+get_tsc_command() {
+ local repo="$1"
+
+ # Dialtone uses Vitest/Mocha/node --test depending on package
+ case "$repo" in
+ packages/dialtone-vue)
+ echo "pnpm nx run dialtone-vue:test"
+ ;;
+ packages/eslint-plugin-dialtone)
+ echo "pnpm nx run eslint-plugin-dialtone:test"
+ ;;
+ packages/stylelint-plugin-dialtone)
+ echo "pnpm nx run stylelint-plugin-dialtone:test"
+ ;;
+ packages/postcss-responsive-variations)
+ echo "pnpm nx run postcss-responsive-variations:test"
+ ;;
+ packages/dialtone-docs)
+ echo "pnpm nx run dialtone-docs:test"
+ ;;
+ *)
+ echo ""
+ ;;
+ esac
+}
+
+# Detect repo
+repo=$(detect_repo "$file_path")
+
+# Skip if unknown repo
+if [[ "$repo" == "unknown" ]] || [[ -z "$repo" ]]; then
+ exit 0 # Exit 0 for skip conditions
+fi
+
+# Log edited file
+printf '%s\t%s\t%s\n' "$(date +%s)" "$file_path" "$repo" >> "$cache_dir/edited-files.log"
+
+# Update affected repos list
+if ! grep -q "^$repo$" "$cache_dir/affected-repos.txt" 2>/dev/null; then
+ echo "$repo" >> "$cache_dir/affected-repos.txt"
+fi
+
+# Store build commands
+build_cmd=$(get_build_command "$repo")
+tsc_cmd=$(get_tsc_command "$repo")
+
+if [[ -n "$build_cmd" ]]; then
+ echo "$repo:build:$build_cmd" >> "$cache_dir/commands.txt.tmp"
+fi
+
+if [[ -n "$tsc_cmd" ]]; then
+ echo "$repo:tsc:$tsc_cmd" >> "$cache_dir/commands.txt.tmp"
+fi
+
+# Remove duplicates from commands
+if [[ -f "$cache_dir/commands.txt.tmp" ]]; then
+ sort -u "$cache_dir/commands.txt.tmp" > "$cache_dir/commands.txt"
+ rm -f "$cache_dir/commands.txt.tmp"
+fi
+
+# Exit cleanly
+exit 0
diff --git a/.claude/hooks/pre-push-pr-guard.sh b/.claude/hooks/pre-push-pr-guard.sh
new file mode 100755
index 0000000000..99dbb86650
--- /dev/null
+++ b/.claude/hooks/pre-push-pr-guard.sh
@@ -0,0 +1,195 @@
+#!/bin/bash
+set -e
+
+# PreToolUse hook โ checks documentation before push and PR creation
+# Uses the edit tracker to see if code files were touched without doc updates
+# Maps package source paths to dialtone-docs content files
+# Push: suggests updating docs if missing
+# PR creation: same + asks about /doc-janitor
+
+tool_info=$(cat)
+
+tool_name=$(echo "$tool_info" | jq -r '.tool_name // empty' 2>/dev/null)
+command=$(echo "$tool_info" | jq -r '.tool_input.command // empty' 2>/dev/null)
+session_id=$(echo "$tool_info" | jq -r '.session_id // empty' 2>/dev/null)
+
+# Only check Bash tool
+if [[ "$tool_name" != "Bash" ]] || [[ -z "$command" ]]; then
+ exit 0
+fi
+
+# Detect push and PR creation
+is_push=false
+is_pr=false
+
+# Split on && and || to check each command in the chain independently
+# This prevents "git stash push && git push" from being classified as stash-only
+IFS=$'\n'
+for cmd_part in $(echo "$command" | sed 's/&&/\n/g; s/||/\n/g; s/|/\n/g'); do
+ # Skip stash push commands
+ if echo "$cmd_part" | grep -qE 'git\s+stash\s+push'; then
+ continue
+ fi
+ if echo "$cmd_part" | grep -qE 'git\s+.*push(\s|$)'; then
+ is_push=true
+ fi
+done
+unset IFS
+
+if echo "$command" | grep -qE '(^|\s|&&|\|)gh\s+.*pr\s+create(\s|$)'; then
+ is_pr=true
+fi
+
+if [[ "$is_push" == "false" ]] && [[ "$is_pr" == "false" ]]; then
+ exit 0
+fi
+
+# Check if checks were already done this session
+project_dir="${CLAUDE_PROJECT_DIR:-$(pwd)}"
+marker_dir="$project_dir/.claude/tsc-cache/${session_id:-default}"
+
+if [[ "$is_pr" == "true" ]]; then
+ marker_file="$marker_dir/pr-create-done"
+else
+ marker_file="$marker_dir/push-done"
+fi
+
+# If marker exists, check if new edits happened after it was created
+if [[ -f "$marker_file" ]]; then
+ log_file_check="$marker_dir/edited-files.log"
+ if [[ -f "$log_file_check" ]] && [[ "$log_file_check" -nt "$marker_file" ]]; then
+ # New edits since last check โ remove marker and re-run
+ rm -f "$marker_file"
+ else
+ exit 0
+ fi
+fi
+
+# Read edit tracker
+log_file="$marker_dir/edited-files.log"
+
+if [[ ! -f "$log_file" ]]; then
+ # No edits tracked โ allow
+ exit 0
+fi
+
+edited_files=$(cut -d$'\t' -f2 < "$log_file")
+
+# Dialtone docs content path
+docs_content="packages/dialtone-docs/src/content"
+
+# Map: source package paths to their corresponding dialtone-docs content files
+# Check if code files were edited in these areas
+has_token_edits=$(echo "$edited_files" | grep -c 'packages/dialtone-tokens/' || true)
+has_css_edits=$(echo "$edited_files" | grep -c 'packages/dialtone-css/' || true)
+has_vue_edits=$(echo "$edited_files" | grep -c 'packages/dialtone-vue/' || true)
+has_icon_edits=$(echo "$edited_files" | grep -c 'packages/dialtone-icons/' || true)
+has_ci_edits=$(echo "$edited_files" | grep -c '\.github/workflows/' || true)
+has_mcp_edits=$(echo "$edited_files" | grep -c 'packages/dialtone-mcp-server/' || true)
+has_eslint_edits=$(echo "$edited_files" | grep -c 'packages/eslint-plugin-dialtone/' || true)
+has_stylelint_edits=$(echo "$edited_files" | grep -c 'packages/stylelint-plugin-dialtone/' || true)
+
+# Check if any corresponding doc files were also edited
+has_doc_edits=$(echo "$edited_files" | grep -c "$docs_content/" || true)
+
+# Build list of packages touched without doc updates
+missing_docs=""
+
+if (( has_token_edits > 0 )); then
+ # Check for token-related docs
+ token_docs=$(echo "$edited_files" | grep -cE "(development-design-tokens|architecture-design-token-pipeline)" || true)
+ if (( token_docs == 0 )); then
+ missing_docs="${missing_docs} - dialtone-tokens โ development-design-tokens.md, architecture-design-token-pipeline.md\n"
+ fi
+fi
+
+if (( has_css_edits > 0 )); then
+ css_docs=$(echo "$edited_files" | grep -c "development-css-utilities" || true)
+ if (( css_docs == 0 )); then
+ missing_docs="${missing_docs} - dialtone-css โ development-css-utilities.md\n"
+ fi
+fi
+
+if (( has_vue_edits > 0 )); then
+ vue_docs=$(echo "$edited_files" | grep -cE "(development-component-workflow|reference-component-api-patterns|reference-accessibility-checklist|development-testing)" || true)
+ if (( vue_docs == 0 )); then
+ missing_docs="${missing_docs} - dialtone-vue โ development-component-workflow.md, reference-component-api-patterns.md\n"
+ fi
+fi
+
+if (( has_icon_edits > 0 )); then
+ icon_docs=$(echo "$edited_files" | grep -c "development-icons" || true)
+ if (( icon_docs == 0 )); then
+ missing_docs="${missing_docs} - dialtone-icons โ development-icons.md\n"
+ fi
+fi
+
+if (( has_ci_edits > 0 )); then
+ ci_docs=$(echo "$edited_files" | grep -c "workflow-ci-pipeline" || true)
+ if (( ci_docs == 0 )); then
+ missing_docs="${missing_docs} - .github/workflows โ workflow-ci-pipeline.md\n"
+ fi
+fi
+
+docs_missing=false
+if [[ -n "$missing_docs" ]]; then
+ docs_missing=true
+fi
+
+# If no issues and not a PR, allow through
+if [[ "$docs_missing" == "false" ]] && [[ "$is_pr" == "false" ]]; then
+ mkdir -p "$marker_dir"
+ touch "$marker_file"
+ exit 0
+fi
+
+mkdir -p "$marker_dir"
+
+CYAN=$'\033[1;36m'; RESET=$'\033[0m'
+
+if [[ "$is_pr" == "true" ]]; then
+ echo -e "${CYAN}๐ PR creation check triggered${RESET}" >&2
+
+ if [[ "$docs_missing" == "true" ]]; then
+ # Write missing docs mapping to cache so the skill can read it
+ echo -e "$missing_docs" > "$marker_dir/missing-docs.txt"
+
+ jq -n \
+ --arg missing "$missing_docs" \
+ --arg log_file "$log_file" \
+ --arg marker_dir "$marker_dir" \
+ --arg marker_file "$marker_file" \
+ '{
+ "hookSpecificOutput": { "permissionDecision": "deny" },
+ "systemMessage": ("Code files were edited this session but the corresponding dialtone-docs content was not updated.\n\nMissing doc updates:\n" + $missing + "\nRun the doc-sync-enforcer skill now. For each mapping above:\n1. Read the source files that were changed (listed in " + $log_file + ")\n2. Read the corresponding doc file in packages/dialtone-docs/src/content/\n3. Update the doc file to reflect the source code changes โ update relevant sections, examples, and the last_updated frontmatter field\n4. If the change is trivial (bug fix, formatting only), note that and skip the doc update\n\nAfter the enforcer finishes, ask the user: \"Want me to run /doc-janitor to clean up stale artifacts before the PR?\"\nIf yes, launch the doc-janitor agent.\nIf no, skip.\n\nThen run:\n mkdir -p " + $marker_dir + " && touch " + $marker_file + "\n\nThen retry the PR creation.")
+ }'
+ else
+ # Docs are up to date โ allow PR creation (pr-fill already suggests /doc-janitor)
+ touch "$marker_file"
+ exit 0
+ fi
+else
+ # Push: only block if docs are missing
+ if [[ "$docs_missing" == "true" ]]; then
+ echo -e "${CYAN}๐ Pre-push doc check triggered${RESET}" >&2
+
+ # Write missing docs mapping to cache so the skill can read it
+ echo -e "$missing_docs" > "$marker_dir/missing-docs.txt"
+
+ jq -n \
+ --arg missing "$missing_docs" \
+ --arg log_file "$log_file" \
+ --arg marker_dir "$marker_dir" \
+ --arg marker_file "$marker_file" \
+ '{
+ "hookSpecificOutput": { "permissionDecision": "deny" },
+ "systemMessage": ("Code files were edited this session but the corresponding dialtone-docs content was not updated.\n\nMissing doc updates:\n" + $missing + "\nRun the doc-sync-enforcer skill now. For each mapping above:\n1. Read the source files that were changed (listed in " + $log_file + ")\n2. Read the corresponding doc file in packages/dialtone-docs/src/content/\n3. Update the doc file to reflect the source code changes โ update relevant sections, examples, and the last_updated frontmatter field\n4. If the change is trivial (bug fix, formatting only), note that and skip the doc update\n\nAfter the enforcer finishes, run:\n mkdir -p " + $marker_dir + " && touch " + $marker_file + "\n\nThen retry the push.")
+ }'
+ else
+ # Docs were updated, allow push
+ touch "$marker_file"
+ exit 0
+ fi
+fi
+
+exit 0
diff --git a/.claude/settings.json b/.claude/settings.json
index 5cbefed8e0..ae021d9eed 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -1,4 +1,10 @@
{
+ "permissions": {
+ "allow": [
+ "Skill(doc-sync-enforcer)",
+ "Agent(doc-janitor)"
+ ]
+ },
"hooks": {
"PreToolUse": [
{
@@ -7,6 +13,10 @@
{
"type": "command",
"command": ".claude/hooks/commit-format.sh"
+ },
+ {
+ "type": "command",
+ "command": ".claude/hooks/pre-push-pr-guard.sh"
}
]
},
@@ -28,6 +38,10 @@
"type": "command",
"command": ".claude/hooks/shaping-ripple.sh",
"timeout": 1000
+ },
+ {
+ "type": "command",
+ "command": ".claude/hooks/post-tool-use-tracker.sh"
}
]
}
diff --git a/.claude/skills/doc-sync-enforcer.md b/.claude/skills/doc-sync-enforcer.md
new file mode 100644
index 0000000000..84b437cc36
--- /dev/null
+++ b/.claude/skills/doc-sync-enforcer.md
@@ -0,0 +1,115 @@
+---
+name: doc-sync-enforcer
+description: Updates dialtone-docs content files when source code changes. Triggered by the pre-push-pr-guard hook or manually via /doc-sync-enforcer. Reads the edit tracker cache to know what changed, maps source packages to doc files, and updates them.
+---
+
+# Documentation Sync Enforcer
+
+Updates `packages/dialtone-docs/src/content/` files to reflect source code changes across the Dialtone monorepo.
+
+## When This Runs
+
+- **Automatically** โ triggered by `pre-push-pr-guard.sh` when code files were edited but corresponding docs were not
+- **Manually** โ via `/doc-sync-enforcer` command
+
+## Source โ Doc Mapping
+
+| Source path | Doc file(s) in `packages/dialtone-docs/src/content/` |
+|---|---|
+| `packages/dialtone-tokens/` | `development/development-design-tokens.md`, `architecture/architecture-design-token-pipeline.md` |
+| `packages/dialtone-css/` | `development/development-css-utilities.md` |
+| `packages/dialtone-vue/` | `development/development-component-workflow.md`, `reference/reference-component-api-patterns.md`, `reference/reference-accessibility-checklist.md`, `development/development-testing.md` |
+| `packages/dialtone-icons/` | `development/development-icons.md` |
+| `.github/workflows/` | `workflows/workflow-ci-pipeline.md` |
+| Release config changes | `workflows/workflow-release-process.md` |
+| Branch strategy changes | `workflows/workflow-branch-strategy.md` |
+| Commit convention changes | `workflows/workflow-conventional-commits.md` |
+
+## Workflow
+
+### Step 1 โ Read the edit tracker
+
+Read the session cache to find what was changed:
+
+```
+.claude/tsc-cache//edited-files.log โ every file edited this session
+.claude/tsc-cache//missing-docs.txt โ mappings flagged by the hook
+.claude/tsc-cache//affected-repos.txt โ which packages were touched
+```
+
+The `edited-files.log` format is `timestamp:filepath:repo` โ extract the filepaths.
+
+### Step 2 โ For each mapping, read source and doc
+
+For each entry in `missing-docs.txt`:
+
+1. **Read the changed source files** from `edited-files.log` that belong to the flagged package
+2. **Read the existing doc file** from `packages/dialtone-docs/src/content/`
+3. **Understand what changed** โ new files added, APIs modified, configs changed, patterns introduced
+
+### Step 3 โ Determine if update is needed
+
+Not every code change needs a doc update:
+
+- **Bug fix / formatting** โ skip, note it
+- **New feature / API / config** โ update the relevant section
+- **Removed feature** โ remove or update the section
+- **New file / pattern** โ add to the doc if significant
+
+### Step 4 โ Update the doc file
+
+When updating a doc file:
+
+1. **Preserve the existing structure** โ the doc has `##` sections that are independently searchable
+2. **Update only the sections affected** by the source code changes
+3. **Update the `last_updated` frontmatter field** to today's date
+4. **Never invent information** โ only document what exists in the actual code
+5. **Keep the same writing style** โ concise, technical, grep-friendly
+
+### Frontmatter format
+
+Every doc file has YAML frontmatter that must be preserved:
+
+```yaml
+---
+type: guide|reference|workflow|architecture|standard
+category: development|architecture|workflows|reference|standards
+keywords:
+ - keyword1
+ - keyword2
+ - keyword3
+ai_summary: "One-line summary, max 150 characters"
+last_updated: "2026-03-13"
+related_packages:
+ - "@dialpad/package-name"
+---
+```
+
+Required fields: `type`, `category`, `keywords` (3+), `ai_summary` (โค150 chars), `last_updated`.
+
+### Step 5 โ Report what was done
+
+After updating, print a summary:
+
+```
+๐ Doc Sync Complete
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+
+โ
Updated:
+ development-design-tokens.md โ added new spacing token section
+ architecture-design-token-pipeline.md โ updated build step 3
+
+โญ๏ธ Skipped (trivial changes):
+ development-css-utilities.md โ bug fix only, no doc impact
+
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+```
+
+## Rules
+
+- **Never invent functionality** โ only document what exists in the actual code
+- **Read the source files first** โ don't guess what changed, read the actual diff or file
+- **Preserve human-authored content** โ when updating, never remove existing sections unless the feature was removed
+- **Keep docs under 350 lines** โ if longer, the doc is trying to cover too much
+- **Use actual package names and technical terms** โ content must be grep-friendly (no placeholders)
+- **File naming convention** โ kebab-case with category prefix: `development-component-workflow.md`
diff --git a/.github/workflows/cleanup-plan-docs.yml b/.github/workflows/cleanup-plan-docs.yml
new file mode 100644
index 0000000000..8a414ff520
--- /dev/null
+++ b/.github/workflows/cleanup-plan-docs.yml
@@ -0,0 +1,39 @@
+name: Cleanup Plan Docs
+
+on:
+ pull_request:
+ types: [closed]
+ branches: [staging]
+
+jobs:
+ cleanup:
+ if: github.event.pull_request.merged == true
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ concurrency:
+ group: ${{ github.workflow }}
+ cancel-in-progress: false
+ steps:
+ - name: Checkout staging
+ uses: actions/checkout@v4
+ with:
+ ref: staging
+ token: ${{ github.token }}
+
+ - name: Configure Git User
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Remove planning docs
+ run: |
+ git rm -f PLAN-*.md --ignore-unmatch
+ git rm -rf docs/plans/ --ignore-unmatch
+
+ - name: Commit and push if changed
+ run: |
+ if ! git diff --cached --quiet --exit-code; then
+ git commit -m "chore: remove planning docs post-merge [skip ci]"
+ git push origin staging
+ fi
diff --git a/.gitignore b/.gitignore
index b7d7aa8091..e51b0dfa03 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,3 +57,26 @@ packages/postcss-responsive-variations/coverage
*.vsix
packages/language-server/sample/*.vue
packages/language-server/sample/*.css
+
+# Claude session cache
+.claude/tsc-cache/
+
+# Work artifacts โ session files generated by Claude during development
+# These are useful locally but should never be committed or pushed.
+# Patterns match at any directory depth.
+PLAN-*.md
+PLAN.md
+*-plan.md
+SESSION_*.md
+*_SESSION.md
+CLEANUP_*.md
+*_CLEANUP.md
+ITERATION_*.md
+VERIFICATION_*.md
+VALIDATION_*.md
+TODO_*.md
+SKILLS_TODO*.md
+*.backup
+*.bak
+*.old
+*.deprecated
diff --git a/CLAUDE.md b/CLAUDE.md
index 96e12e8d97..48fa659df4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -111,6 +111,36 @@ When creating or updating a component, ALL must stay in sync:
- Workflow: feature branch โ PR to `staging` โ semantic-release โ `production` fast-forward
- Config: `release-ci.config.cjs` per package
+## Doc Sync Tooling
+
+Hooks and tools that keep `packages/dialtone-docs/src/content/` in sync with source code changes. Requires the `dialtone-docs` package (PR #1051) to be merged.
+
+### How it works
+
+1. **Edit tracker** (`.claude/hooks/post-tool-use-tracker.sh`) โ PostToolUse hook that silently logs every file edit to `.claude/tsc-cache//edited-files.log` with affected packages and NX commands.
+2. **Pre-push guard** (`.claude/hooks/pre-push-pr-guard.sh`) โ PreToolUse hook on Bash that fires on `git push` / `gh pr create`. Maps source packages to doc content files. Denies push if docs are missing and triggers the enforcer.
+3. **Doc sync enforcer** โ Skill (`/doc-sync-enforcer`) that reads the edit log, maps changes to `dialtone-docs` content files, and updates them.
+4. **Doc janitor** โ Agent (`/doc-janitor`) that sweeps stale artifacts (plan files, session files, backups) before merging.
+
+### Package โ doc mapping
+
+| Source package | Doc content file |
+| --- | --- |
+| `packages/dialtone-tokens/` | `development-design-tokens.md`, `architecture-design-token-pipeline.md` |
+| `packages/dialtone-css/` | `development-css-utilities.md` |
+| `packages/dialtone-vue/` | `development-component-workflow.md`, `reference-component-api-patterns.md` |
+| `packages/dialtone-icons/` | `development-icons.md` |
+| `.github/workflows/` | `workflow-ci-pipeline.md` |
+
+### Session cache
+
+All tracking data lives in `.claude/tsc-cache//` (gitignored):
+- `edited-files.log` โ tab-separated: `timestamp\tfilepath\trepo`
+- `affected-repos.txt` โ one package per line
+- `commands.txt` โ NX build/test commands for affected packages
+- `missing-docs.txt` โ written by the guard when docs are missing
+- `push-done` / `pr-create-done` โ marker files to avoid re-checking
+
## Key Files Reference
| File | Purpose |