From c3557a3d9796fd3546e7b9925e0d4ad7cf1b9b42 Mon Sep 17 00:00:00 2001 From: Paul O'Reilly Date: Thu, 19 Mar 2026 10:54:49 +1300 Subject: [PATCH] Add formatters, hooks, memory entries and context-load improvements - formatters/js, md, yaml: new/updated formatter scripts - hooks/set-wezterm-profile.sh: new hook for WezTerm profile switching - scripts/context-load: improvements from recent sessions - HOOKS.md, MEMORY.md: updated documentation and index entries - memory/log, memory/reference-infrastructure-docs.md: session logs and reference Co-Authored-By: Claude Sonnet 4.6 --- HOOKS.md | 16 +++++++++ MEMORY.md | 4 +++ formatters/js | 26 ++++++++++++++- formatters/md | 20 ++++++++++- formatters/yaml | 20 ++++++++++- hooks/set-wezterm-profile.sh | 44 +++++++++++++++++++++++++ memory/log/2026-03-18.002319.md | 23 +++++++++++++ memory/reference-infrastructure-docs.md | 40 ++++++++++++++++++++++ scripts/context-load | 10 ++++++ 9 files changed, 200 insertions(+), 3 deletions(-) mode change 120000 => 100755 formatters/js mode change 120000 => 100755 formatters/md mode change 120000 => 100755 formatters/yaml create mode 100755 hooks/set-wezterm-profile.sh create mode 100644 memory/log/2026-03-18.002319.md create mode 100644 memory/reference-infrastructure-docs.md diff --git a/HOOKS.md b/HOOKS.md index 3b25369..c87bb4c 100644 --- a/HOOKS.md +++ b/HOOKS.md @@ -9,6 +9,7 @@ Claude Code hooks that run automatically in response to events. Source of truth | Pre-compact backup | PreCompact (auto + manual) | `hooks/pre-compact-backup.sh` | | Post-edit lint | PostToolUse (Edit/Write/MultiEdit) | `hooks/post-edit-lint.sh` | | Pre-commit lint | Git pre-commit | `hooks/pre-commit-lint.sh` | +| WezTerm profile theme | SessionStart (per-profile) | `hooks/set-wezterm-profile.sh` | ## Post-edit Lint @@ -92,6 +93,21 @@ This symlinks all hooks into `~/.claude/hooks/` and prints the `settings.json` c scripts/setup-formatters.sh [ ...] ``` +## WezTerm Profile Theme + +Sets the WezTerm terminal theme when a Claude session starts or resumes, by emitting an OSC 1337 `SetUserVar` escape sequence. WezTerm fires its `user-var-changed` Lua event and applies the matching color scheme and background image from `wezterm.lua`. + +**Derives profile name** from `CLAUDE_CONFIG_DIR` (e.g. `~/.claude-octopus` → `octopus`). No-ops for the default profile. + +**Writes to `/dev/tty`** to reach WezTerm directly, bypassing Claude Code's stdout capture. Safe in non-WezTerm terminals — the sequence is silently ignored. + +**Configure per profile** in `~/.claude-/settings.json`: +```json +"SessionStart": [ + { "hooks": [{ "type": "command", "command": "~/.claude/hooks/set-wezterm-profile.sh" }] } +] +``` + ## Adding New Hooks 1. Create the script in `hooks/` diff --git a/MEMORY.md b/MEMORY.md index d9a3983..b1e3d8b 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -23,6 +23,10 @@ - [/context-load](memory/skill-context-load.md) — Reload project context after /clear or mid-session context loss - [/housekeeping](memory/skill-housekeeping.md) — Cross-project health check: git status, unreflected logs, skill validation, pipeline recommendations +## References + +- [Infrastructure Docs Repo](memory/reference-infrastructure-docs.md) — Shared infrastructure catalog at ~/dev/claude/docs/infrastructure/homelab — system descriptions and dependency map for the homelab + ## Topic Files - [Decisions](memory/decisions.md) — Architecture and design decisions: pipeline design, linting system, context-load, CLAUDE.md structure diff --git a/formatters/js b/formatters/js deleted file mode 120000 index b3c373c..0000000 --- a/formatters/js +++ /dev/null @@ -1 +0,0 @@ -ts \ No newline at end of file diff --git a/formatters/js b/formatters/js new file mode 100755 index 0000000..d6cc66b --- /dev/null +++ b/formatters/js @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Formatter: TypeScript/JavaScript — biome format + biome check +# Contract: $1 = absolute file path, format in place, lint, exit 0 if clean / 1 if errors +# Missing tools: exit 0 silently + +FILE="$1" +[ -z "$FILE" ] && exit 0 +[ -f "$FILE" ] || exit 0 + +command -v biome >/dev/null 2>&1 || exit 0 + +# Format in place +biome format --write "$FILE" 2>/dev/null + +# Auto-fix what we can +biome check --fix "$FILE" 2>/dev/null + +# Final lint pass — remaining errors go to stderr +ERRORS=$(biome check "$FILE" 2>&1) +if [ $? -ne 0 ]; then + echo "$ERRORS" >&2 + exit 1 +fi + +exit 0 diff --git a/formatters/md b/formatters/md deleted file mode 120000 index c87a0a0..0000000 --- a/formatters/md +++ /dev/null @@ -1 +0,0 @@ -json \ No newline at end of file diff --git a/formatters/md b/formatters/md new file mode 100755 index 0000000..d8c2eb4 --- /dev/null +++ b/formatters/md @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Formatter: JSON/YAML/Markdown — prettier +# Contract: $1 = absolute file path, format in place, lint, exit 0 if clean / 1 if errors +# Missing tools: exit 0 silently + +FILE="$1" +[ -z "$FILE" ] && exit 0 +[ -f "$FILE" ] || exit 0 + +command -v prettier >/dev/null 2>&1 || exit 0 + +# Format in place (prettier auto-detects parser from extension) +ERRORS=$(prettier --write "$FILE" 2>&1) +if [ $? -ne 0 ]; then + echo "$ERRORS" >&2 + exit 1 +fi + +exit 0 diff --git a/formatters/yaml b/formatters/yaml deleted file mode 120000 index c87a0a0..0000000 --- a/formatters/yaml +++ /dev/null @@ -1 +0,0 @@ -json \ No newline at end of file diff --git a/formatters/yaml b/formatters/yaml new file mode 100755 index 0000000..d8c2eb4 --- /dev/null +++ b/formatters/yaml @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Formatter: JSON/YAML/Markdown — prettier +# Contract: $1 = absolute file path, format in place, lint, exit 0 if clean / 1 if errors +# Missing tools: exit 0 silently + +FILE="$1" +[ -z "$FILE" ] && exit 0 +[ -f "$FILE" ] || exit 0 + +command -v prettier >/dev/null 2>&1 || exit 0 + +# Format in place (prettier auto-detects parser from extension) +ERRORS=$(prettier --write "$FILE" 2>&1) +if [ $? -ne 0 ]; then + echo "$ERRORS" >&2 + exit 1 +fi + +exit 0 diff --git a/hooks/set-wezterm-profile.sh b/hooks/set-wezterm-profile.sh new file mode 100755 index 0000000..c4a3a61 --- /dev/null +++ b/hooks/set-wezterm-profile.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# set-wezterm-profile.sh — SessionStart hook: emit WezTerm theme escape sequence +# +# Derives the profile name from CLAUDE_CONFIG_DIR (e.g. ~/.claude-octopus → octopus) +# and emits a SetUserVar escape sequence so WezTerm fires its user-var-changed +# event and applies the matching theme from wezterm.lua. +# +# Hook subprocesses have no controlling terminal, so the escape sequence is +# written to the pts device found by walking up the process tree. +# Safe in non-WezTerm terminals (escape sequence is silently ignored). +# +# Usage (settings.json): +# "SessionStart": [{ "hooks": [{ "type": "command", +# "command": "~/.claude/hooks/set-wezterm-profile.sh" }] }] + +set -euo pipefail + +profile_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" +profile_name="${profile_dir##*/.claude-}" + +# No profile suffix means this is the default profile — nothing to do +if [[ "$profile_name" == "$profile_dir" ]]; then + exit 0 +fi + +# Emit the escape sequence to the terminal device. +# Hook subprocesses have no controlling terminal (/dev/tty unavailable), +# so walk up the process tree to find the first ancestor with a pts device. + +tty_device="" +pid=$$ +while [[ "$pid" -gt 1 ]]; do + pts=$(ps -o tty= -p "$pid" 2>/dev/null | tr -d ' ') + if [[ -n "$pts" && "$pts" != "?" && -w "/dev/$pts" ]]; then + tty_device="/dev/$pts" + break + fi + pid=$(ps -o ppid= -p "$pid" 2>/dev/null | tr -d ' ') +done + +if [[ -n "$tty_device" ]]; then + printf '\e]1337;SetUserVar=%s=%s\a' CLAUDE_PROFILE \ + "$(printf '%s' "$profile_name" | base64)" >"$tty_device" 2>/dev/null || true +fi diff --git a/memory/log/2026-03-18.002319.md b/memory/log/2026-03-18.002319.md new file mode 100644 index 0000000..63e834d --- /dev/null +++ b/memory/log/2026-03-18.002319.md @@ -0,0 +1,23 @@ +# Session Log — 2026-03-18 + +## Summary +Created a shared infrastructure catalog repo (`homelab/infrastructure-docs`) documenting all homelab systems with dependency tracking. 20 system files covering physical hosts, cluster core, networking, identity, platform services, and applications. + +## Decisions +- Decision: Infrastructure knowledge goes in a dedicated repo, not MEMORY.md — Rationale: Infrastructure maps are authoritative and maintained (add/remove systems), not accumulated learnings. Different lifecycle than gotchas/reflections. +- Decision: Located at `~/dev/claude/docs/infrastructure/homelab/` with parent structure supporting future locations (e.g., customer sites) — Rationale: User expects to have infrastructure at multiple logical locations in future. +- Decision: Repo in `homelab` org on Gitea, not `skynet` — Rationale: Infrastructure documentation, not AI-focused. +- Decision: Each system file uses consistent structure (what it does, depends on, depended on by, managed by) — Rationale: Enables tracing dependencies in either direction. + +## Gotchas Discovered +- **[gitea]** Symptom: Push to new repo failed with "User permission denied for writing" — Fix: The SSH alias `gitea.oreillyit.nz-homelab` authenticates as `cluster-administrator`, but repo was created by `ai_admin` via API. Added `cluster-administrator` as admin collaborator via API before push succeeded. + +## Key Context +- `ai_admin` Gitea token is in `cluster-bootstrap/local_secrets/gitea_ai_admin` (not in `~/dev/claude/secrets/`) +- Created repo via Gitea API: `POST /api/v1/orgs/homelab/repos` +- Added collaborator via API: `PUT /api/v1/repos/homelab/infrastructure-docs/collaborators/cluster-administrator` +- Memory reference added to both claude-foundations MEMORY.md and Claude profile memory + +## Process Notes +- Good pattern: discussing the conceptual approach (MEMORY vs SPEC vs dedicated catalog) before jumping into implementation +- The Gitea permission issue could be avoided by always adding `cluster-administrator` as collaborator when creating repos via `ai_admin` API token for the `homelab` org diff --git a/memory/reference-infrastructure-docs.md b/memory/reference-infrastructure-docs.md new file mode 100644 index 0000000..b536a13 --- /dev/null +++ b/memory/reference-infrastructure-docs.md @@ -0,0 +1,40 @@ +# Infrastructure Docs Repo + +## Location + +- Filesystem: `~/dev/claude/docs/infrastructure/homelab/` +- Repo: `gitea.oreillyit.nz/homelab/infrastructure-docs` +- SSH remote: `git@gitea.oreillyit.nz-homelab:homelab/infrastructure-docs.git` + +## Purpose + +Authoritative catalog of all systems in the homelab environment. Unlike MEMORY.md (accumulated learnings) or CLAUDE.md (project conventions), this is a **maintained map** — updated when systems are added/removed, not accumulated over time. + +Answers: "What depends on X?", "What breaks if Y goes down?", "What manages Z?" + +## Structure + +- `INDEX.md` — thin index listing all system files, grouped by category +- One `.md` per system (e.g., `cilium.md`, `traefik.md`, `authelia.md`) +- Each file: what it does, network details, depends on, depended on by, managed by +- Parent `docs/infrastructure/` supports multiple locations (future: `customer-site-x/`) + +## Categories + +- Physical/Hosting: proxmox, vps, bootstrap-vm +- Cluster Core: talos, cilium, argocd +- Storage: proxmox-csi, proxmox-ccm +- Networking: wireguard, caddy, traefik, coredns, dnsmasq +- Identity: authelia, headscale +- Platform: gitea, email-relay, homepage, cert-pipeline +- Applications: octopus-deploy, external-services + +## Source projects + +Documents systems from both `cluster-bootstrap` and `cluster-apps`. Shared across projects — not inside any single project repo. + +## When to update + +- Adding/removing/replacing a system +- Dependencies change (new service depends on Authelia, etc.) +- Major version upgrades worth noting diff --git a/scripts/context-load b/scripts/context-load index 2f51f3f..b6400e4 100755 --- a/scripts/context-load +++ b/scripts/context-load @@ -80,6 +80,16 @@ if [[ ${#claude_dirs[@]} -eq 0 ]]; then echo "# Searched from: $PWD" >&2 fi +# --- Output: Claude profile --- + +profile_dir="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" +profile_name="$(basename "$profile_dir")" +if [[ "$profile_name" == ".claude" ]]; then + emit_header "CLAUDE PROFILE: default (~/.claude)" +else + emit_header "CLAUDE PROFILE: $profile_name ($profile_dir)" +fi + # --- Output: CLAUDE.md files (top-down) + tree from each --- # Track files we've already emitted to avoid duplicates