From f736660e7fd4c0adac1f1c1088b2475d452b0ff4 Mon Sep 17 00:00:00 2001 From: notque Date: Fri, 24 Jul 2026 18:38:28 -0700 Subject: [PATCH] refactor(skills): fold 11 language/shell/k8s husk skills into umbrella references Folds husks whose content already lives in the umbrella they point at: - kotlin-coroutines (0.93), kotlin-testing (0.97) -> engineering/kotlin/references/ - swift-concurrency (0.87), swift-testing (0.96) -> engineering/swift/references/ - php-quality, php-testing -> engineering/php/references/ - fish-shell-config (0.87), zsh-shell-config (0.91) -> infrastructure/shell-config/references/ - kubernetes-debugging, kubernetes-security -> infrastructure/kubernetes/references/ - cobalt-core (promoted_to: kubernetes) -> infrastructure/kubernetes/references/cobalt-*.md Every husk reference file was verified byte-identical (difflib ratio 1.000) against its umbrella counterpart before removal. Where the husk SKILL.md body held unique guidance not yet in the umbrella (php-quality, php-testing, fish, zsh), that content was merged into the umbrella reference first; the umbrella index files are condensed rewrites, which is why SKILL.md-level ratios are lower than the reference-level ones. Umbrellas are unchanged in scope - the kubernetes references retain the private infra knowledge. Inbound references updated across docs, hooks/README, migration scripts, and shell-config routing tables. Skills: 131 -> 120. --- README.md | 4 +- docs/docs-audit.md | 2 +- docs/for-knowledge-workers.md | 2 +- docs/for-linkedin.md | 2 +- docs/injected-context-contracts.md | 4 +- hooks/README.md | 2 +- scripts/fix-skill-paths.py | 11 - scripts/migrate-skills-to-folders.py | 11 - skills/engineering/cobalt-core/SKILL.md | 87 ---- .../references/concurrency-patterns.md | 268 ---------- .../cobalt-core/references/kvm-exporter.md | 463 ------------------ .../references/testing-patterns.md | 271 ---------- skills/engineering/kotlin-coroutines/SKILL.md | 71 --- .../references/channel-patterns.md | 50 -- .../references/concurrency-patterns.md | 181 ------- .../references/flow-patterns.md | 85 ---- .../references/preferred-patterns.md | 69 --- skills/engineering/kotlin-testing/SKILL.md | 291 ----------- skills/engineering/php-quality/SKILL.md | 91 ---- .../references/framework-idioms.md | 83 ---- .../references/modern-php-features.md | 197 -------- .../php-quality/references/quality-tools.md | 52 -- skills/engineering/php-testing/SKILL.md | 72 --- .../php-testing/references/patterns.md | 301 ------------ .../engineering/php/references/php-quality.md | 28 ++ .../engineering/php/references/php-testing.md | 2 + skills/engineering/swift-concurrency/SKILL.md | 46 -- .../references/actor-isolation.md | 91 ---- .../references/fundamentals.md | 88 ---- .../references/preferred-patterns.md | 100 ---- .../references/task-patterns.md | 141 ------ skills/engineering/swift-testing/SKILL.md | 263 ---------- .../infrastructure/fish-shell-config/SKILL.md | 299 ----------- .../references/bash-migration.md | 149 ------ .../references/fish-preferred-patterns.md | 240 --------- .../references/fish-quick-reference.md | 229 --------- .../references/tool-integrations.md | 318 ------------ .../kubernetes-debugging/SKILL.md | 86 ---- .../references/crash-diagnosis.md | 162 ------ .../references/network-debugging.md | 63 --- .../references/resource-debugging.md | 104 ---- .../kubernetes-security/SKILL.md | 79 --- .../references/network-policies.md | 88 ---- .../references/pod-security.md | 98 ---- .../references/rbac-patterns.md | 65 --- .../references/supply-chain.md | 119 ----- skills/infrastructure/kubernetes/SKILL.md | 2 +- .../kubernetes/references/cobalt-core.md | 7 + .../references/kubernetes-debugging.md | 8 +- .../references/kubernetes-security.md | 15 + .../references/network-debugging.md | 2 +- .../infrastructure/public-web-deploy/SKILL.md | 2 +- .../service-health-check/SKILL.md | 2 +- .../references/fish-shell-config.md | 21 +- .../references/zsh-preferred-patterns.md | 1 + .../references/zsh-shell-config.md | 14 +- .../shell-process-patterns/SKILL.md | 2 +- .../infrastructure/zsh-shell-config/SKILL.md | 389 --------------- .../references/bash-migration.md | 248 ---------- .../references/tool-integrations.md | 368 -------------- .../references/zsh-preferred-patterns.md | 289 ----------- .../references/zsh-quick-reference.md | 332 ------------- skills/meta/do/SKILL.md | 4 +- skills/meta/skill-creator/SKILL.md | 2 +- .../references/domain-research-targets.md | 2 +- 65 files changed, 102 insertions(+), 7136 deletions(-) delete mode 100644 skills/engineering/cobalt-core/SKILL.md delete mode 100644 skills/engineering/cobalt-core/references/concurrency-patterns.md delete mode 100644 skills/engineering/cobalt-core/references/kvm-exporter.md delete mode 100644 skills/engineering/cobalt-core/references/testing-patterns.md delete mode 100644 skills/engineering/kotlin-coroutines/SKILL.md delete mode 100644 skills/engineering/kotlin-coroutines/references/channel-patterns.md delete mode 100644 skills/engineering/kotlin-coroutines/references/concurrency-patterns.md delete mode 100644 skills/engineering/kotlin-coroutines/references/flow-patterns.md delete mode 100644 skills/engineering/kotlin-coroutines/references/preferred-patterns.md delete mode 100644 skills/engineering/kotlin-testing/SKILL.md delete mode 100644 skills/engineering/php-quality/SKILL.md delete mode 100644 skills/engineering/php-quality/references/framework-idioms.md delete mode 100644 skills/engineering/php-quality/references/modern-php-features.md delete mode 100644 skills/engineering/php-quality/references/quality-tools.md delete mode 100644 skills/engineering/php-testing/SKILL.md delete mode 100644 skills/engineering/php-testing/references/patterns.md delete mode 100644 skills/engineering/swift-concurrency/SKILL.md delete mode 100644 skills/engineering/swift-concurrency/references/actor-isolation.md delete mode 100644 skills/engineering/swift-concurrency/references/fundamentals.md delete mode 100644 skills/engineering/swift-concurrency/references/preferred-patterns.md delete mode 100644 skills/engineering/swift-concurrency/references/task-patterns.md delete mode 100644 skills/engineering/swift-testing/SKILL.md delete mode 100644 skills/infrastructure/fish-shell-config/SKILL.md delete mode 100644 skills/infrastructure/fish-shell-config/references/bash-migration.md delete mode 100644 skills/infrastructure/fish-shell-config/references/fish-preferred-patterns.md delete mode 100644 skills/infrastructure/fish-shell-config/references/fish-quick-reference.md delete mode 100644 skills/infrastructure/fish-shell-config/references/tool-integrations.md delete mode 100644 skills/infrastructure/kubernetes-debugging/SKILL.md delete mode 100644 skills/infrastructure/kubernetes-debugging/references/crash-diagnosis.md delete mode 100644 skills/infrastructure/kubernetes-debugging/references/network-debugging.md delete mode 100644 skills/infrastructure/kubernetes-debugging/references/resource-debugging.md delete mode 100644 skills/infrastructure/kubernetes-security/SKILL.md delete mode 100644 skills/infrastructure/kubernetes-security/references/network-policies.md delete mode 100644 skills/infrastructure/kubernetes-security/references/pod-security.md delete mode 100644 skills/infrastructure/kubernetes-security/references/rbac-patterns.md delete mode 100644 skills/infrastructure/kubernetes-security/references/supply-chain.md delete mode 100644 skills/infrastructure/zsh-shell-config/SKILL.md delete mode 100644 skills/infrastructure/zsh-shell-config/references/bash-migration.md delete mode 100644 skills/infrastructure/zsh-shell-config/references/tool-integrations.md delete mode 100644 skills/infrastructure/zsh-shell-config/references/zsh-preferred-patterns.md delete mode 100644 skills/infrastructure/zsh-shell-config/references/zsh-quick-reference.md diff --git a/README.md b/README.md index e2c0c7196..c8e81efca 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ AI agents skip steps. Harnesses have a second problem: given only a skill list, they do not route eagerly enough, or correctly enough. Good skills sit unused. So this toolkit connects the skills, agents, and workflows we want directly into the harness, automatically. You don't have to understand what is here. Say what you want in plain English and you get all the value we have put into it: the right specialist with the right methodology, behind gates that demand exit codes, not assertions. -44 domain agents, 131 workflow skills, 86 hooks, 129 scripts. Agents carry knowledge, skills enforce methodology, hooks block incomplete work, scripts handle determinism. +44 domain agents, 120 workflow skills, 86 hooks, 129 scripts. Agents carry knowledge, skills enforce methodology, hooks block incomplete work, scripts handle determinism. Works across Claude Code (`/do`), Codex (`$do`), Factory (`/do`), Reasonix (`/do`). @@ -148,7 +148,7 @@ Strips built-in tool-use instructions. The toolkit's agents, skills, hooks, and | Layer | Count | Does | |---|---|---| | Agents | 44 | Domain knowledge: idiom tables, failure mode catalogs, error-to-fix mappings | -| Skills | 131 | Phased methodology with gates. Can't skip steps. Each phase has exit criteria requiring evidence. | +| Skills | 120 | Phased methodology with gates. Can't skip steps. Each phase has exit criteria requiring evidence. | | Hooks | 88 | Fire on lifecycle events. Block incomplete work. Zero LLM cost. | | Scripts | 129 | Determinism: test runners, linters, validators. No LLM judgment. | diff --git a/docs/docs-audit.md b/docs/docs-audit.md index 362643ba3..206365d8c 100644 --- a/docs/docs-audit.md +++ b/docs/docs-audit.md @@ -16,7 +16,7 @@ Work order for follow-up PRs. Covers every `docs/` file outside the value-clarit | `what-didnt-work.md` | Future sessions, maintainers | Negative-results registry: failed experiments with evidence and decisions | None. Load-bearing for evidence-gated evolution; format is enforced and queryable. | keep | | `QUICKSTART.md` | New users | 30-second start: install, `/do`, mental model | Duplicates start-here.md (install, entry-point table, first commands, verify steps). No inbound links from README or any other doc; orphaned. Two starts confuse the funnel. | merge-into-start-here.md, then retire | | `REFERENCE.md` | Users wanting the full command list | "Everything in one place. Scan in 2 minutes." | Hand-maintained catalog; drifts from `INDEX.json` truth as components land. Only inbound link is QUICKSTART.md (itself a retire candidate). | rewrite: generate from `scripts/routing-manifest.py`; absorb QUICKSTART's "want the full list" pointer | -| `skills.md` | Users browsing the skill catalog | Full catalog of 131 skills by category | Hand-maintained; same drift risk as REFERENCE.md. README links it, so it must stay accurate. | rewrite: generate via `scripts/generate-skill-index.py` output (script, not prose pass) | +| `skills.md` | Users browsing the skill catalog | Full catalog of 120 skills by category | Hand-maintained; same drift risk as REFERENCE.md. README links it, so it must stay accurate. | rewrite: generate via `scripts/generate-skill-index.py` output (script, not prose pass) | | `for-claude-code.md` | LLMs operating in the repo | Machine-dense inventory: paths, schemas, conventions | Fit for purpose; value framing would be wasted on a machine audience. Minor: repo map lists `~/private-skills/` inside the repo tree, which misstates its location. | keep (fix the one map line in a follow-up) | | `for-linkedin.md` | Humans who enjoy the joke | Parody of AI-influencer launch posts | The AI patterns are the content. De-AI editing or value rewriting would destroy the parody. Counts (44/124/83) currently match validator truth. | keep | | `CITATIONS.md` | Maintainers | Provenance of patterns and prior art | None. Low traffic but cheap to keep and useful for "why is it built this way". | keep | diff --git a/docs/for-knowledge-workers.md b/docs/for-knowledge-workers.md index 8801b66c3..c72b95647 100644 --- a/docs/for-knowledge-workers.md +++ b/docs/for-knowledge-workers.md @@ -8,7 +8,7 @@ read_when: ## What This Gives You -You describe work in plain English. The system routes it to the right pipeline, with quality checks built in. 131 skills behind a single command, and you never need to know which one fired. +You describe work in plain English. The system routes it to the right pipeline, with quality checks built in. 120 skills behind a single command, and you never need to know which one fired. ## Interface diff --git a/docs/for-linkedin.md b/docs/for-linkedin.md index eb23da9c0..d0faf5e2e 100644 --- a/docs/for-linkedin.md +++ b/docs/for-linkedin.md @@ -75,7 +75,7 @@ A year of daily use on real work. Finding gaps, filling them, watching the syste The result: - 44 domain specialist agents across languages, infrastructure, review, research, content -- 131 workflow skills covering everything from TDD to article writing to Reddit moderation +- 120 workflow skills covering everything from TDD to article writing to Reddit moderation - 83 lifecycle hooks that fire at session boundaries to inject context, capture learnings, enforce gates - A learning database that tracks patterns and graduates them into agent behavior - Parallel review pipelines that catch issues before they reach production diff --git a/docs/injected-context-contracts.md b/docs/injected-context-contracts.md index 933eeed05..ab947151d 100644 --- a/docs/injected-context-contracts.md +++ b/docs/injected-context-contracts.md @@ -104,11 +104,11 @@ Source: `hooks/sapcc-go-detector.py`. Meaning: A SAP Commerce Cloud Go project was detected in the current directory. Action: Apply SAP CC Go conventions for the session. The `go-patterns` and `sapcc-review` skills are in scope. -### `[fish-shell] Detected Fish shell user` plus `[auto-skill] fish-shell-config` +### `[fish-shell] Detected Fish shell user` plus `[auto-skill] shell-config` Source: `hooks/fish-shell-detector.py`. Meaning: The user runs Fish as their interactive shell. -Action: When the user asks for shell config edits, prefer Fish syntax (`set -gx`, `function`, `~/.config/fish/config.fish`) over Bash/Zsh idioms. The `fish-shell-config` skill carries the full reference. +Action: When the user asks for shell config edits, prefer Fish syntax (`set -gx`, `function`, `~/.config/fish/config.fish`) over Bash/Zsh idioms. The `shell-config` skill carries the full reference. ### `[adr-health-check] Active ADR session` plus `domain` and `adr` path diff --git a/hooks/README.md b/hooks/README.md index 1d437d2d1..8d5bd2750 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -32,7 +32,7 @@ Task completes: TaskCompleted |------|-------------| | `afk-mode` | Injects autonomous behavioral posture for unattended sessions (SSH, tmux, or `CLAUDE_AFK_MODE=always`). See [AFK Mode docs](afk-mode/README.md) | | `cross-repo-agents` | Discovers local `.claude/agents/` in the working directory and injects them for `/do` routing | -| `fish-shell-detector` | Detects Fish shell users and injects the `fish-shell-config` skill | +| `fish-shell-detector` | Detects Fish shell users and injects the `shell-config` skill | | `hook-version-parity-check` | Warn-only: compares deployed `~/.claude/hooks/*.py` hook-version headers against the repo checkout; on drift names the hooks and the sync command | | `session-github-briefing` | Injects GitHub monitoring briefing into session context (opt-in: `CLAUDE_KAIROS_ENABLED=true`) | | `operator-context-detector` | Detects operator context (personal/work/ci/production) and injects behavioral profile | diff --git a/scripts/fix-skill-paths.py b/scripts/fix-skill-paths.py index 0c802bd99..16693bf7c 100755 --- a/scripts/fix-skill-paths.py +++ b/scripts/fix-skill-paths.py @@ -53,18 +53,11 @@ # live in ~/private-skills, not in this repo. "enterprise-search": "engineering", "go-patterns": "engineering", - "kotlin-coroutines": "engineering", - "kotlin-testing": "engineering", "kotlin": "engineering", - "php-quality": "engineering", - "php-testing": "engineering", "php": "engineering", - "swift-concurrency": "engineering", - "swift-testing": "engineering", "swift": "engineering", "sapcc-audit": "engineering", "sapcc-review": "engineering", - "cobalt-core": "engineering", "distinctive-frontend-design": "frontend", "frontend-slides": "frontend", "threejs-builder": "frontend", @@ -74,11 +67,7 @@ "game-sprite-pipeline": "game", "phaser-gamedev": "game", "motion-pipeline": "game", - "kubernetes-debugging": "infrastructure", - "kubernetes-security": "infrastructure", "kubernetes": "infrastructure", - "fish-shell-config": "infrastructure", - "zsh-shell-config": "infrastructure", "shell-config": "infrastructure", "shell-process-patterns": "infrastructure", "headless-cron-creator": "infrastructure", diff --git a/scripts/migrate-skills-to-folders.py b/scripts/migrate-skills-to-folders.py index f9a063937..b9bead313 100755 --- a/scripts/migrate-skills-to-folders.py +++ b/scripts/migrate-skills-to-folders.py @@ -55,18 +55,11 @@ # engineering/ — language-specific patterns + domain engineering "enterprise-search": "engineering", "go-patterns": "engineering", - "kotlin-coroutines": "engineering", - "kotlin-testing": "engineering", "kotlin": "engineering", - "php-quality": "engineering", - "php-testing": "engineering", "php": "engineering", - "swift-concurrency": "engineering", - "swift-testing": "engineering", "swift": "engineering", "sapcc-audit": "engineering", "sapcc-review": "engineering", - "cobalt-core": "engineering", "cli-design": "engineering", "opensearch-detection-engineer": "engineering", # frontend/ @@ -82,11 +75,7 @@ "phaser-gamedev": "game", "motion-pipeline": "game", # infrastructure/ — ops, k8s, shell, cron - "kubernetes-debugging": "infrastructure", - "kubernetes-security": "infrastructure", "kubernetes": "infrastructure", - "fish-shell-config": "infrastructure", - "zsh-shell-config": "infrastructure", "shell-config": "infrastructure", "shell-process-patterns": "infrastructure", "headless-cron-creator": "infrastructure", diff --git a/skills/engineering/cobalt-core/SKILL.md b/skills/engineering/cobalt-core/SKILL.md deleted file mode 100644 index 62c6837fb..000000000 --- a/skills/engineering/cobalt-core/SKILL.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -name: cobalt-core -promoted_to: kubernetes -description: "Cobalt Core infrastructure knowledge: KVM exporters, hypervisor tooling, OpenStack compute." -agent: kubernetes-helm-engineer -user-invocable: true -argument-hint: "" -allowed-tools: - - Read - - Grep - - Glob - - Bash - - Agent - - Edit - - Write -routing: - triggers: - - "cobalt core" - - "cobaltcore" - - "kvm-exporter" - - "kvm exporter" - - "hypervisor metrics" - - "libvirt exporter" - - "cloud hypervisor" - category: infrastructure - pairs_with: - - go-patterns - - prometheus-grafana-engineer - - kubernetes-helm-engineer ---- - -# Cobalt Core - -Domain skill for the [cobaltcore-dev](https://github.com/cobaltcore-dev) project family — SAP Converged Cloud infrastructure components for KVM hypervisor management, metrics collection, and compute-node tooling. - -## Reference Loading Table - -| Signal | Reference | Size | -|--------|-----------|------| -| kvm-exporter, metrics, prometheus, libvirt, hypervisor, collector, scrape, steal time, NUMA, cgroups, cloud hypervisor | `references/kvm-exporter.md` | ~800 lines | -| goroutine, concurrency, semaphore, TryLock, sync.Map, race condition, socket exhaustion, scrape overlap, ClearScrapeCache | `references/concurrency-patterns.md` | ~200 lines | -| test, mock, moq, unit test, E2E, Kind cluster, race detector, interface_mock_gen, test-metrics.sh | `references/testing-patterns.md` | ~200 lines | - -**Loading rule.** Read the references whose signals match the task before responding. - ---- - -## Phase 1: IDENTIFY - -Determine which cobaltcore component the user is asking about. - -| Component | Repository | Reference | -|-----------|-----------|-----------| -| KVM Exporter | `cobaltcore-dev/kvm-exporter` | `references/kvm-exporter.md` | - -If the component is not listed, tell the user no reference exists yet and offer to analyze the repo. - -**Gate**: Component identified. Reference loaded. Proceed to Phase 2. - ---- - -## Phase 2: RESPOND - -Use loaded reference knowledge to answer the user's question. The references contain: -- Architecture and data flow diagrams -- Complete metric catalogs with types, labels, and descriptions -- Configuration options and environment variables -- Deployment models (Helm, DaemonSet, container specs) -- Code patterns (concurrency, caching, error handling) -- Testing strategies (unit mocks, E2E with Kind clusters) -- Alerting rules and operational concerns - -For implementation questions involving Go code, pair with the `go-patterns` skill for language-specific patterns. For Prometheus/Grafana questions, pair with `prometheus-grafana-engineer`. For Kubernetes deployment questions, pair with `kubernetes-helm-engineer`. - -**Gate**: Question answered with reference-backed specifics, not generic advice. - ---- - -## Phase 3: EXTEND - -When the user wants to add a new cobaltcore repo: -1. Analyze the repo systematically (README, go.mod, key source files, Dockerfile, Helm chart) -2. Create a new reference file at `references/{repo-name}.md` -3. Update the Reference Loading Table in this SKILL.md -4. Update the component table in Phase 1 - -Follow the structure established in `references/kvm-exporter.md` for consistency. diff --git a/skills/engineering/cobalt-core/references/concurrency-patterns.md b/skills/engineering/cobalt-core/references/concurrency-patterns.md deleted file mode 100644 index 70dce47b1..000000000 --- a/skills/engineering/cobalt-core/references/concurrency-patterns.md +++ /dev/null @@ -1,268 +0,0 @@ -# Cobalt Core — Concurrency Patterns - -> **Scope**: Goroutine management, synchronization primitives, and scrape-safe concurrency as used in kvm-exporter. Does not cover general Go concurrency theory. -> **Version range**: Go 1.21+ (uses `sync.Mutex.TryLock`, generics-ready patterns) -> **Generated**: 2026-04-16 — verify against `internal/libvirt/` source - ---- - -## Overview - -kvm-exporter collects metrics from 50–500+ KVM domains per scrape, each requiring libvirt RPC calls, /proc reads, and cgroup stat lookups. The concurrency model is built around: (1) goroutine-per-domain with a semaphore cap to prevent socket exhaustion, (2) `TryLock` to serialize overlapping Prometheus scrapes, and (3) tiered caching with `sync.Map` for cross-scrape state. Violating these patterns causes libvirt socket exhaustion, metric duplication, or stale data. - ---- - -## Pattern Table - -| Pattern | Version | Use When | Avoid When | -|---------|---------|----------|------------| -| `sync.Mutex.TryLock()` | Go 1.18+ | Scrape serialization — skip if already collecting | Long-held business locks | -| Buffered channel semaphore | All | Cap goroutines to protect shared resource (libvirt socket) | Global rate limits (use `golang.org/x/sync/semaphore`) | -| `sync.Map` | Go 1.9+ | Per-domain delta state that survives scrapes | Simple per-request scratch space (use plain map + mutex) | -| `atomic.Value` | All | Single-value timestamp for readiness probe | Multi-field structs (use mutex instead) | -| `context.WithTimeout` | All | Bound collection to 40s scrape deadline | Calling `context.Background()` directly in collection | - ---- - -## Correct Patterns - -### Semaphore-Limited Goroutines (the kvm-exporter pattern) - -Cap concurrent domain collections to prevent libvirt socket exhaustion. The semaphore is a buffered channel of empty structs. - -```go -sem := make(chan struct{}, 50) // max 50 concurrent domain goroutines - -var wg sync.WaitGroup -for _, domain := range domains { - wg.Add(1) // Add BEFORE go func to avoid race with wg.Wait() - go func(d libvirt.Domain) { - defer wg.Done() - sem <- struct{}{} // acquire slot - defer func() { <-sem }() // release slot - collectDomain(ctx, d, ch) - }(domain) -} -wg.Wait() -``` - -**Why**: libvirt's Unix socket is a single RPC endpoint. Sending 500 simultaneous calls causes libvirt to drop connections. The semaphore keeps in-flight calls bounded without serializing all collection. - ---- - -### TryLock for Scrape Serialization - -Use `TryLock` to skip a scrape when the previous one is still running, rather than blocking Prometheus. - -```go -type ServiceImpl struct { - mu sync.Mutex -} - -func (s *ServiceImpl) Collect(ch chan<- prometheus.Metric) { - if !s.mu.TryLock() { - // Previous scrape still running — emit nothing, Prometheus retries - return - } - defer s.mu.Unlock() - s.retrieveMetrics(ctx, ch) -} -``` - -**Why**: If collection takes 35s and Prometheus scrapes every 30s, blocking stacks goroutines until OOM. `TryLock` sacrifices one scrape rather than accumulating blocked callers. - -**Version note**: `sync.Mutex.TryLock()` added in Go 1.18. - ---- - -### sync.Map for Cross-Scrape Delta State - -Steal time requires comparing current CPU time to the previous scrape's value. `sync.Map` provides concurrent-safe access without a mutex wrapping the domain loop. - -```go -// Key: "domainName-PID" -var stealTimeHistory sync.Map - -func calculateStealTime(domain, pid string, current uint64) float64 { - key := domain + "-" + pid - if prev, ok := stealTimeHistory.Load(key); ok { - delta := current - prev.(uint64) - stealTimeHistory.Store(key, current) - return float64(delta) - } - stealTimeHistory.Store(key, current) - return 0 // first scrape — no delta available -} -``` - -**Why**: The domain loop runs concurrently. A plain `map[string]uint64` requires a global mutex serializing all delta lookups. `sync.Map` is optimized for read-heavy workloads with stable key sets (domain IDs don't churn rapidly). - ---- - -### Context Cancellation in Collection Loops - -Check `ctx.Err()` before queuing each domain to exit early when the scrape timeout fires. - -```go -for _, domain := range domains { - if ctx.Err() != nil { - return // timeout fired before we could queue this domain - } - wg.Add(1) - go func(d libvirt.Domain) { - defer wg.Done() - sem <- struct{}{} - defer func() { <-sem }() - if ctx.Err() != nil { - return // re-check inside goroutine before doing any work - } - collectDomain(ctx, d, ch) - }(domain) -} -``` - -**Why**: Without the pre-queue check, all 500 domains could be queued before the timeout fires, then all goroutines start and run over deadline together. - ---- - -## Pattern Catalog - -### Cap Goroutines with a Semaphore - -**Detection**: -```bash -# Find goroutine launches — review each for semaphore acquire -rg 'go func\(' --type go internal/ -grep -rn 'go func(' --include="*.go" internal/ -``` - -**Signal**: -```go -for _, domain := range domains { - go func(d libvirt.Domain) { - collectDomain(ctx, d, ch) // no semaphore - }(domain) -} -``` - -**Why this matters**: On a hypervisor with 500 VMs, this launches 500 goroutines simultaneously. Each goroutine makes libvirt RPC calls over the same Unix socket. libvirt queues drop, connections time out, and the exporter logs hundreds of "connection refused" per scrape. - -**Preferred action**: Add buffered channel semaphore with cap ≤ 50 as shown in Correct Patterns above. - ---- - -### Use TryLock in Prometheus Collect Path - -**Detection**: -```bash -rg '\.Lock\(\)' --type go internal/libvirt/ -grep -n "\.Lock()" internal/libvirt/*.go -``` -Review if `Lock()` appears in any `Collect()` or `Describe()` method. - -**Signal**: -```go -func (s *ServiceImpl) Collect(ch chan<- prometheus.Metric) { - s.mu.Lock() // blocks if previous scrape running - defer s.mu.Unlock() - s.retrieveMetrics(ctx, ch) -} -``` - -**Why this matters**: Prometheus's default scrape timeout is 10s. If collection takes 35s and Prometheus fires every 30s, the second `Collect()` call blocks, then the third — goroutines stack until the scrape target appears hung and alerts fire. - -**Preferred action**: Replace `s.mu.Lock()` with `if !s.mu.TryLock() { return }`. - -**Version note**: `TryLock` added in Go 1.18. For earlier versions, use `sync/atomic` swap-based implementation. - ---- - -### Defer ClearScrapeCache() at Function Start - -**Detection**: -```bash -rg 'ClearScrapeCache' --type go -grep -rn 'ClearScrapeCache' --include="*.go" . -``` -Confirm it is called in `defer` or in all exit paths of `retrieveMetrics`. - -**Signal**: -```go -func (s *ServiceImpl) retrieveMetrics(ctx context.Context, ch chan<- prometheus.Metric) { - if err := s.connectLibvirt(); err != nil { - log.Errorf("connect: %v", err) - return // scrape cache NOT cleared — stale data persists - } - // ... - s.ch.ClearScrapeCache() // only reached on success -} -``` - -**Why this matters**: Per-scrape caches (PID lookup, VM counters) accumulate stale entries. On the next successful scrape, the exporter reads stale PIDs for domains restarted since last scrape, emitting metrics for dead processes. - -**Preferred action**: `defer s.ch.ClearScrapeCache()` at the top of `retrieveMetrics`, before any error paths. - ---- - -### Use sync.Map for Concurrent Domain State - -**Detection**: -```bash -rg 'map\[string\]' --type go internal/libvirt/ -grep -n "map\[string\]" internal/libvirt/*.go -``` -Any `map[string]` accessed from goroutines without a wrapping mutex is a data race. - -**Signal**: -```go -var history = map[string]uint64{} // shared, no mutex - -func updateHistory(key string, val uint64) uint64 { - prev := history[key] // concurrent read — data race - history[key] = val // concurrent write — data race - return val - prev -} -``` - -**Why this matters**: Go's race detector (`go test -race`) catches this. In production without `-race`, it causes silent map corruption or `panic: concurrent map read and map write`. - -**Preferred action**: Use `sync.Map` for stable read-heavy key sets (domain IDs), or a `sync.RWMutex`-protected map if you need `range` iteration. - ---- - -## Error-Fix Mappings - -| Error | Root Cause | Fix | -|-------|-----------|-----| -| `panic: concurrent map read and map write` | Goroutine-per-domain accessing plain `map` | Replace with `sync.Map` or add `sync.RWMutex` | -| `libvirt: connection refused` (mass, same scrape) | Too many concurrent goroutines saturating libvirt socket | Verify semaphore is applied; reduce cap if needed | -| `context deadline exceeded` every scrape | Collection regularly exceeds 40s timeout | Enable pprof (`ENABLE_PPROF=true`); common culprit is blocked libvirt RPC without per-call timeout | -| Metrics missing for some domains each scrape | `wg.Add(1)` called inside goroutine rather than before launch | Move `wg.Add(1)` to before `go func(...)` | - ---- - -## Detection Commands Reference - -```bash -# Find unbounded goroutine launches (check each for semaphore) -rg 'go func\(' --type go internal/ - -# Find blocking Lock() in Prometheus Collect path -rg '\.Lock\(\)' --type go internal/libvirt/ - -# Find plain map in concurrent code -rg 'map\[string\]' --type go internal/libvirt/ - -# Find missing or misplaced ClearScrapeCache calls -rg 'ClearScrapeCache' --type go - -# Run race detector across all packages -go test -race ./internal/... -``` - ---- - -## See Also - -- `references/kvm-exporter.md` — Concurrency Model and Cache Tiers sections for architecture overview -- `references/testing-patterns.md` — How to write race-detector-clean tests with moq diff --git a/skills/engineering/cobalt-core/references/kvm-exporter.md b/skills/engineering/cobalt-core/references/kvm-exporter.md deleted file mode 100644 index ba69460df..000000000 --- a/skills/engineering/cobalt-core/references/kvm-exporter.md +++ /dev/null @@ -1,463 +0,0 @@ -# kvm-exporter Reference - -Repository: [cobaltcore-dev/kvm-exporter](https://github.com/cobaltcore-dev/kvm-exporter) -Language: Go 1.23+ (module `github.com/cobaltcore-dev/kvm-exporter`) -License: Apache-2.0 -Current version: 1.3.0 (Helm chart 0.3.8) - -Prometheus metrics exporter collecting hypervisor-level statistics from KVM hosts running QEMU or Cloud Hypervisor (CH). Gathers data via libvirt, cgroups v2, `/proc`, and CH HTTP socket API. Designed for SAP Converged Cloud OpenStack Nova compute nodes. - ---- - -## Architecture - -### Directory Structure - -``` -cmd/ - server/main.go # Entry point — HTTP server with chi router - playground/main.go # Development playground -internal/ - config/ # Environment-based configuration (envconfig) - globals/ # HTTP endpoint constants - libvirt/ # Core: all collectors, CH integration, metrics - log/ # Structured logging (logrus wrapper) - middlewares/ # HTTP readiness probe middleware - numareader/ # NUMA topology reader (/proc/pid/numa_maps, sysfs numastat) - procreader/ # Process reader (/proc: schedstat, cmdline) -chart/ # Helm chart (DaemonSet, ServiceMonitor, alerts, dashboards) -config/ # Kustomize deployment configs -test/ # E2E infrastructure (Kind cluster with VMs) -``` - -### Data Flow - -``` - Prometheus scrape - | - GET /metrics - | - promhttp.Handler() - | - ServiceImpl.Collect() - | - RetrieveMetrics(ctx) - | - +-----------+-------------+----------+-----------+ - | | | | | - libvirt cgroups v2 /proc fs CH socket sysfs - (RPC) (cgroup2) (procreader) (HTTP API) (numa) - | | | | | - +-----------+-------------+----------+-----------+ - | - chan<- prometheus.Metric -``` - -### Entry Point (`cmd/server/main.go`) - -1. Reads config via `config.ReadConfig()` (env vars + optional libvirt.conf) -2. Sets GC to 50%, memory limit to 450 MiB (tuned for memory-constrained pods) -3. Creates `libvirt.ServiceImpl` (the Prometheus collector) -4. Registers with `prometheus.MustRegister()` -5. Sets up chi router: `/health`, `/ready`, `/metrics`, `/debug/pprof/*` (optional) -6. Starts HTTP server (default port 8080) -7. Graceful shutdown on SIGINT/SIGTERM - -### Collector Architecture - -`ServiceImpl` implements `prometheus.Collector` (Describe/Collect). On each scrape: - -1. **Connect** to libvirt via Unix socket if not connected -2. **List all domains** (active + inactive) via `ConnectListAllDomains` -3. **Parallel /proc scan** — `UpdateAllProcesses()` finds VM processes -4. **Bulk domain stats** — single `ConnectGetAllDomainStats` RPC (QEMU only) -5. **Concurrent domain collection** — goroutines per domain (semaphore-limited to 50): - domain info, vCPU, block, network, memory, uptime, steal time, hugepages, NUMA -6. **Node-level collection** — storage pools, libvirt version, NIC bonding, CPU SMT, host NUMA -7. **Stale cleanup** — remove cached data for inactive domains -8. **Aggregation** — weighted average steal time across all domains - -### Performance Optimizations - -| Technique | Purpose | -|-----------|---------| -| Bulk `ConnectGetAllDomainStats` | Replace N per-domain RPCs with one call | -| XML cache (5-min TTL) | Eliminate XML desc RPCs per scrape | -| Block I/O tune cache (5-min TTL) | Reduce I/O tune lookups | -| Concurrency semaphore (50 goroutines) | Prevent libvirt socket exhaustion | -| Collection timeout (40s) | Context cancellation on slow scrapes | -| `sync.Mutex.TryLock()` | Prevent overlapping scrapes | -| `debug.FreeOSMemory()` | Explicit memory return after collection | - ---- - -## Configuration - -All via environment variables (`kelseyhightower/envconfig`): - -| Env Variable | Type | Default | Purpose | -|---|---|---|---| -| `SERVICE_NAME` | string | `kvm-exporter` | Service identifier in logs | -| `APP` | string | `kvm-exporter` | Application name | -| `ENV` | string | `LOCAL` | Environment (LOCAL = debug text, else JSON info) | -| `PORT` | int | `8080` | HTTP listen port | -| `LIBVIRT_SOCKET` | string | `/run/libvirt/libvirt-sock-ro` | Libvirt Unix socket path | -| `LIBVIRT_URI` | string | `ch:///system` | Libvirt connection URI | -| `LIBVIRT_CONF_PATH` | string | `/etc/libvirt/libvirt.conf` | Optional libvirt config | -| `CLOUDHYPERVISOR_SOCKET_PATH` | string | `/run/libvirt/ch` | CH socket base directory | -| `EXTERNAL_MOUNT_PATHS` | []string | `/var/lib/nova/mnt,/var/lib/nova/instances` | Disk usage mount paths | -| `DISABLED_COLLECTORS` | []string | (empty) | Collectors to disable | -| `HOSTNAME` | string | (from env) | Node hostname | -| `ENABLE_PPROF` | string | (unset) | `true` enables pprof endpoints | - -### Valid Collector Names for `DISABLED_COLLECTORS` - -`domain_info`, `vcpu`, `network`, `memory`, `block`, `block_limits`, `storage_pools`, `uptime`, `steal_time`, `hugepages`, `version`, `nic_bonding`, `cpu_smt`, `domain_numa`, `node_numa` - -### Hypervisor Detection - -`isCloudHypervisor()` checks if `LIBVIRT_URI` starts with `ch://`. Drives conditional logic throughout all collectors. - ---- - -## Metric Catalog - -### Domain Info (`domain_info`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_domain_libvirt_info` | Gauge | node, domain, uuid, instance_name, flavor, user_name, user_uuid, project_name, project_uuid, root_type, root_uuid | -| `kvm_domain_libvirt_info_maximum_memory_bytes` | Gauge | node, domain | -| `kvm_domain_libvirt_info_memory_usage_bytes` | Gauge | node, domain | -| `kvm_domain_libvirt_info_virtual_cpus` | Gauge | node, domain | -| `kvm_domain_libvirt_info_cpu_time_seconds_total` | Counter | node, domain | -| `kvm_domain_libvirt_info_vstate` | Gauge | node, domain | - -vstate values: 0=no state, 1=running, 2=blocked, 3=paused, 4=shutting down, 5=shut off, 6=crashed, 7=suspended - -### vCPU (`vcpu`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_domain_libvirt_vcpu_delay_nanoseconds` | Counter | node, domain, vcpu, cpu_index | -| `kvm_domain_libvirt_vcpu_spent_nanoseconds` | Counter | node, domain, vcpu, cpu_index | -| `kvm_domain_libvirt_vcpu_timeslices_total` | Counter | node, domain, vcpu, cpu_index | -| `kvm_domain_libvirt_vcpu_time_seconds_sum` | Counter | node, domain | -| `kvm_domain_libvirt_vcpu_online_count` | Gauge | node, domain | -| `kvm_domain_libvirt_vcpu_blocked_count` | Gauge | node, domain | - -### Network (`network`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_domain_libvirt_interface_stats_receive_bytes_total` | Counter | node, domain, target_device | -| `kvm_domain_libvirt_interface_stats_receive_packets_total` | Counter | node, domain, target_device | -| `kvm_domain_libvirt_interface_stats_receive_errors_total` | Counter | node, domain, target_device | -| `kvm_domain_libvirt_interface_stats_receive_drops_total` | Counter | node, domain, target_device | -| `kvm_domain_libvirt_interface_stats_transmit_bytes_total` | Counter | node, domain, target_device | -| `kvm_domain_libvirt_interface_stats_transmit_packets_total` | Counter | node, domain, target_device | -| `kvm_domain_libvirt_interface_stats_transmit_errors_total` | Counter | node, domain, target_device | -| `kvm_domain_libvirt_interface_stats_transmit_drops_total` | Counter | node, domain, target_device | - -### Memory (`memory`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_domain_libvirt_memory_stats_major_fault_total` | Counter | node, domain | -| `kvm_domain_libvirt_memory_stats_minor_fault_total` | Counter | node, domain | -| `kvm_domain_libvirt_memory_stats_unused_bytes` | Gauge | node, domain | -| `kvm_domain_libvirt_memory_stats_available_bytes` | Gauge | node, domain | -| `kvm_domain_libvirt_memory_stats_actual_balloon_bytes` | Gauge | node, domain | -| `kvm_domain_libvirt_memory_stats_rss_bytes` | Gauge | node, domain | -| `kvm_domain_libvirt_memory_stats_usable_bytes` | Gauge | node, domain | -| `kvm_domain_libvirt_memory_stats_disk_cache_bytes` | Gauge | node, domain | -| `kvm_domain_libvirt_memory_stats_used_percent` | Gauge | node, domain | - -Memory stats fallback chain: libvirt API → CH cgroup stats → /proc/PID/status - -### Block Device (`block`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_domain_libvirt_block_device_info` | Gauge | node, domain, device_name, path, allocation, capacity, physical | - -### Block Limits (`block_limits`) - -19 Gauge metrics with labels `node, domain, target_device` covering I/O throttling: -- `kvm_domain_libvirt_block_stats_limit_{total,write,read}_bytes` -- `kvm_domain_libvirt_block_stats_limit_{total,write,read}_requests` -- `kvm_domain_libvirt_block_stats_limit_size_iops_bytes` -- `kvm_domain_libvirt_block_stats_limit_burst_{total,write,read}_bytes` -- `kvm_domain_libvirt_block_stats_limit_burst_{total,write,read}_requests` -- `kvm_domain_libvirt_block_stats_limit_burst_{total,write,read}_bytes_length_seconds` -- `kvm_domain_libvirt_block_stats_limit_burst_length_{total,write,read}_requests_seconds` - -### Storage Pools (`storage_pools`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_domain_libvirt_pool_info_capacity_bytes` | Gauge | pool | -| `kvm_domain_libvirt_pool_info_allocation_bytes` | Gauge | pool | -| `kvm_domain_libvirt_pool_info_available_bytes` | Gauge | pool | - -### Uptime (`uptime`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_domain_libvirt_uptime_seconds` | Gauge | node, domain | - -### Steal Time (`steal_time`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_domain_libvirt_steal_time` | Gauge | node, domain | -| `kvm_node_hypervisor_steal_time` | Gauge | node | - -Per-domain steal time is percentage-based (delta between scrapes). Node-level is weighted average by vCPU count. - -### Version (`version`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_node_libvirt_versions_info` | Gauge | node, libvirtd_running, libvirt_library, libvirt_uri | - -### NIC Bonding (`nic_bonding`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_node_nic_bonding_info` | Gauge | node, master, slave_interface, status | - -status: 1=up, 0=down - -### CPU SMT (`cpu_smt`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_node_cpu_smt_info` | Gauge | node, smt_active, smt_control | - -### Hugepages (`hugepages`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_domain_hugepages_bytes` | Gauge | node, domain | - -From /proc/PID/smaps `Private_Hugetlb`. - -### NUMA (`domain_numa`, `node_numa`) - -| Metric | Type | Labels | -|---|---|---| -| `kvm_domain_numa_memory_bytes` | Gauge | node, domain, numa_node | -| `kvm_node_numa_hit_total` | Counter | node, numa_node | -| `kvm_node_numa_miss_total` | Counter | node, numa_node | -| `kvm_node_numa_foreign_total` | Counter | node, numa_node | -| `kvm_node_numa_local_total` | Counter | node, numa_node | -| `kvm_node_numa_other_total` | Counter | node, numa_node | -| `kvm_node_numa_interleave_hit_total` | Counter | node, numa_node | - -### Internal Operational Metrics - -| Metric | Type | Description | -|---|---|---| -| `functions_processed` | Counter | Processed function calls | -| `jobs_errors` | Counter | Error counter | -| `methods_called_seconds` | Histogram | Per-method latency (labels: source, method, node) | - ---- - -## Cloud Hypervisor Integration - -`CloudhypervisorWrapper` provides an alternative data path for CH-based VMs. - -### Connection - -Per-domain Unix socket at `{socketPath}/{domainName}-socket`. HTTP client with 15s timeout. - -### Cgroup Stats - -Reads cgroup v2 via `containerd/cgroups/v3`. Includes workaround for containerd library bug where `Stat()` fails to read `cpu.stat` for leaf cgroups — reads the file directly as fallback. - -### PID Lookup Strategies - -1. Cgroup `cgroup.procs` file -2. Domstatus XML at `/run/libvirt/ch/{domain}.xml` -3. Process name matching via /proc - -### Key Differences from QEMU - -| Aspect | QEMU | Cloud Hypervisor | -|--------|------|------------------| -| Stats source | libvirt RPC | HTTP API (vm.info, vm.counters) | -| Cgroup path | `machine.slice` | `machine` | -| CPU time | libvirt stats | Cgroup-based | -| Memory stats | libvirt API | Cgroup + /proc fallback | -| Socket | Single libvirt socket | Per-domain socket | - -### Caching - -- **Per-scrape caches**: PID, VM counters — cleared via `ClearScrapeCache()` -- **Cross-scrape caches**: config, qemu-img info, df source — 5-minute TTL - ---- - -## Deployment - -### Container Image - -- Base: `gcr.io/distroless/cc:nonroot` -- CGO_ENABLED=1 (required for libvirt) -- Bundled tools: `ps`, `df`, `qemu-img`, `nsenter`, `cat`, `ls` -- UID/GID: `42438:42438` (kvm-node-agent user) -- Registry: `ghcr.io/cobaltcore-dev/kvm-exporter` - -### Kubernetes DaemonSet - -```yaml -hostPID: true # Read /proc of host VM processes -hostNetwork: true # Host network access -securityContext: - runAsUser: 0 - readOnlyRootFilesystem: true - capabilities: - add: [SYS_PTRACE] # /proc reading - drop: [ALL] -appArmorProfile: - type: Unconfined # Required for libvirt operations -``` - -Volume mounts: `/run/libvirt` (ro), `/sys/fs/cgroup` (ro), libvirt.conf, Nova mount paths. - -### Health Probes - -| Probe | Endpoint | Behavior | -|-------|----------|----------| -| Liveness | `GET /health` | Chi heartbeat (200 if alive) | -| Readiness | `GET /ready` | 503 if last collection > 5 minutes ago (`atomic.Value`) | - ---- - -## Alerts - -Built-in alerts in `chart/alerts/exporter.yaml`: - -| Alert | Severity | Condition | -|---|---|---| -| `KvmNodeLibvirtMetricsMissing` | warning | `up{job="kvm-exporter"} == 0` for 15m | -| `KvmNodeMissingKvmExporter` | critical | Ready node has no kvm-exporter job for 5m | -| `KvmExporterScrapeDurationHigh` | warning | `scrape_duration_seconds > 30` for 15m | -| `KvmNodeLibvirtNotResponding` | critical | GardenLinux node without version_info for 10m | - ---- - -## Dependencies - -### Runtime - -| Dependency | Purpose | -|---|---| -| `digitalocean/go-libvirt` | Pure Go libvirt RPC client (no CGO bindings) | -| `prometheus/client_golang` | Prometheus metrics | -| `Tinkoff/libvirt-exporter` | Libvirt XML schema types | -| `containerd/cgroups/v3` | Cgroup v2 reader | -| `go-chi/chi/v5` | HTTP router | -| `kelseyhightower/envconfig` | Env-based config | -| `sirupsen/logrus` | Structured logging | -| `shirou/gopsutil/v3` | System info (disk usage) | -| `tklauser/go-sysconf` | Sysconf for clock ticks | -| `coreos/go-systemd/v22` | Systemd unit name escaping | -| `prometheus/procfs` | /proc filesystem helpers | - -### Development - -| Tool | Version | Purpose | -|---|---|---| -| `golangci-lint` | 1.57.2 | 18 linters enabled | -| `moq` | 0.5.0 | Interface mock generation | -| `kustomize` | 5.4.1 | K8s manifest management | -| `controller-gen` | 0.15.0 | CRD generation | -| `setup-envtest` | release-0.18 | Test environment setup | - ---- - -## Code Patterns - -### Interface-Based Design - -`LibVirt` and `Cloudhypervisor` interfaces allow mock injection for testing. Mocks generated with `moq`. - -### Metric Naming Convention - -- Domain-scoped libvirt: `kvm_domain_libvirt_` prefix (via `newLibvirtDesc()`) -- Non-libvirt domain: `kvm_domain_` prefix (via `newRawDesc()`) -- Node-level: `kvm_node_` prefix (via `newRawDesc()`) - -### Error Handling - -Errors logged with structured fields (runID, domain, collector name). Collection continues for other domains — no panics in collection path. - -### Context Cancellation - -`ctx.Err()` checked at multiple points in collection for early exit when scrape timeout approaches. - -### Delta Calculations - -Steal time uses `sync.Map` keyed by `"domainName-PID"`, calculating deltas between scrapes. - -### Cache Tiers - -| Tier | Lifetime | Examples | -|------|----------|----------| -| Per-scrape | Cleared via `ClearScrapeCache()` | PID, VM counters | -| Cross-scrape (TTL) | 5-minute TTL | XML desc, block I/O tune | -| Persistent | `sync.Once` | Boot time | - -### Concurrency Model - -| Mechanism | Purpose | -|-----------|---------| -| `sync.Map` | Shared state (steal time history, caches) | -| `sync.Mutex.TryLock()` | Scrape serialization | -| Buffered channel semaphore | Goroutine limiting (50 max) | -| `atomic.Value` | Thread-safe timestamp (readiness probe) | - ---- - -## Testing - -### Unit Tests - -- Standard Go testing with `testify/assert` -- Mock-based via `moq`-generated `InterfaceMock` (`interface_mock_gen.go`) -- Pattern: create `ServiceImpl` with mocked `virt`, inject metric channel, call collector, verify - -### E2E Tests - -- Custom Kind cluster image with libvirt + Cloud Hypervisor (`test/kind/Dockerfile`) -- Test VMs created on worker nodes -- `make test-all` — build image, set up cluster, deploy DaemonSets, create VMs, validate metrics -- `test/test-metrics.sh` — validates expected metrics in HTTP response -- Separate `test-qemu` and `test-ch` targets - ---- - -## CI/CD Workflows - -| Workflow | Purpose | -|---|---| -| `app-test.yaml` | Go tests | -| `app-push.yaml` | Build and push container image | -| `app-test-image-build.yaml` | Build test Kind node image | -| `chart-push.yaml` / `chart-validate.yaml` | Helm chart CI | -| `validate-pr.yaml` | PR validation | -| `validate-prometheus-alerts.yaml` | Alert rule validation | -| `bump.yaml` | Automated version bumping | -| `release.yaml` | Release creation | -| `reuse.yaml` | REUSE compliance | -| `stale.yaml` | Stale issue cleanup | - -### Versioning - -SemVer in `VERSION` file. PR title markers for automated bumps: -- `[BUGFIX]` — patch bump -- `[FEATURE]` — minor bump -- `[BREAKING_CHANGE]` — major bump diff --git a/skills/engineering/cobalt-core/references/testing-patterns.md b/skills/engineering/cobalt-core/references/testing-patterns.md deleted file mode 100644 index a316834bc..000000000 --- a/skills/engineering/cobalt-core/references/testing-patterns.md +++ /dev/null @@ -1,271 +0,0 @@ -# Cobalt Core — Testing Patterns - -> **Scope**: Unit testing with moq-generated mocks and E2E testing with Kind clusters, as used in kvm-exporter. Does not cover general Go testing theory. -> **Version range**: Go 1.21+, moq 0.5.0, Kind 0.23+ -> **Generated**: 2026-04-16 — verify against `test/` and `internal/libvirt/*_test.go` - ---- - -## Overview - -kvm-exporter has two test layers: unit tests using `moq`-generated interface mocks (fast, no libvirt), and E2E tests using a custom Kind cluster image with actual libvirt + Cloud Hypervisor running (slow, requires Docker). The unit test pattern is interface injection — `ServiceImpl` accepts mock `LibVirt` and `Cloudhypervisor` dependencies. The E2E layer validates that expected metrics actually appear in the HTTP response. Skipping the race detector on unit tests is the most common error that lets concurrency bugs through. - ---- - -## Pattern Table - -| Tool | Version | Use When | Avoid When | -|------|---------|----------|------------| -| `moq` | 0.5.0 | Generate mocks for `LibVirt`, `Cloudhypervisor` interfaces | Hand-rolling mocks (use `moq` for regeneration) | -| `testify/assert` | All | Readable assertion failures | `t.Fatalf` for non-fatal checks | -| `go test -race` | All | All concurrent code; CI always | Benchmarks (adds overhead) | -| Kind E2E | 0.23+ | Validating actual metric output end-to-end | Unit-testable logic (too slow) | -| `test/test-metrics.sh` | — | HTTP response metric validation in E2E | Go test for HTTP parsing | - ---- - -## Correct Patterns - -### Unit Test: Interface Injection with moq - -Create `ServiceImpl` with mocked interfaces, inject a metric channel, call the collector, verify emitted metrics. - -```go -func TestVcpuCollector(t *testing.T) { - virt := &LibVirtMock{ - ConnectGetAllDomainStatsFunc: func(...) ([]libvirt.DomainStats, error) { - return []libvirt.DomainStats{ - { - Domain: libvirt.Domain{Name: "test-domain"}, - Vcpu: []libvirt.DomainStatsVcpu{{State: 1, Time: 1000}}, - }, - }, nil - }, - } - - svc := &ServiceImpl{virt: virt} - ch := make(chan prometheus.Metric, 10) - - svc.collectVcpu(context.Background(), ch) - close(ch) - - var metrics []prometheus.Metric - for m := range ch { - metrics = append(metrics, m) - } - assert.NotEmpty(t, metrics, "expected at least one vcpu metric") -} -``` - -**Why**: No libvirt socket required. The mock records call arguments so you can assert the RPC was called with correct parameters. Regenerate with `make generate` after interface changes. - ---- - -### Regenerating Mocks After Interface Change - -After modifying `LibVirt` or `Cloudhypervisor` interfaces, regenerate mocks: - -```bash -# In repo root -make generate -# or directly: -moq -out internal/libvirt/interface_mock_gen.go \ - internal/libvirt LibVirt Cloudhypervisor -``` - -The generated file is committed — never edit `interface_mock_gen.go` by hand. - -**Why**: Hand-edited mocks drift from the interface. `moq` regeneration guarantees the mock matches the interface exactly, and CI will fail if the generated file is out of sync. - ---- - -### E2E Test: Kind Cluster + metric validation - -The E2E setup creates a Kind cluster with a custom node image containing libvirt and Cloud Hypervisor. VMs are created on worker nodes, the exporter DaemonSet is deployed, and metrics are validated via HTTP. - -```bash -# Full E2E (QEMU + CH paths): -make test-all - -# QEMU path only: -make test-qemu - -# CH path only: -make test-ch - -# Manual metric check against running exporter: -curl -s http://NODE_IP:8080/metrics | grep 'kvm_domain_libvirt_vcpu' -``` - -Metric validation script (`test/test-metrics.sh`) uses grep patterns against the HTTP response — add expected metrics there when adding new collectors. - -```bash -# Add to test/test-metrics.sh for a new metric: -check_metric "kvm_domain_libvirt_new_metric_name" -``` - -**Why**: The unit tests mock libvirt responses. The E2E layer validates that the actual libvirt RPC, cgroup reads, and /proc parsing produce real metrics. It's the only layer that catches libvirt version incompatibilities. - ---- - -### Race-Detector-Clean Tests - -Always run the race detector in CI. For local development, add `-race` to catch issues before push. - -```bash -# Run with race detector -go test -race ./internal/... - -# Run specific test with race detector -go test -race -run TestCollectVcpu ./internal/libvirt/ - -# Run with verbose output and race detector -go test -race -v ./internal/libvirt/ -``` - -When using `sync.Map` or channels in tests, the race detector validates the access patterns are safe. - ---- - -## Pattern Catalog - -### Regenerate Mocks via make generate - -**Detection**: -```bash -git log --oneline internal/libvirt/interface_mock_gen.go -grep -n "hand" internal/libvirt/interface_mock_gen.go -rg 'interface_mock_gen' --type go -``` -Check if the file has commits that don't come from `make generate`. - -**Signal**: -```go -// In interface_mock_gen.go — manually added method: -func (m *LibVirtMock) NewMethod(ctx context.Context) error { - // hand-written — not from moq - return nil -} -``` - -**Why this matters**: The next `make generate` overwrites the file. The hand-written method disappears silently. Tests that relied on it compile but the mock no longer has the behavior. - -**Preferred action**: Extend the actual `LibVirt` interface, then run `make generate`. The mock is always derived from the interface. - ---- - -### Run Race Detector on All Concurrent Tests - -**Detection**: -```bash -rg 'go test' Makefile -grep -n "go test" Makefile -``` -Check that all `go test` invocations in Makefile include `-race`. - -**Signal**: -```makefile -test: - go test ./internal/... # no -race -``` - -**Why this matters**: kvm-exporter's domain collection is concurrent. Tests that exercise collection without `-race` can pass even with data races — the race detector is not enabled by default. Race conditions surface only under load in production. - -**Preferred action**: -```makefile -test: - go test -race ./internal/... -``` - -**Version note**: `-race` requires CGO on Linux. Since kvm-exporter already requires `CGO_ENABLED=1` for libvirt, this is always available. - ---- - -### Assert Metric Presence, Not Exact Count - -**Detection**: -```bash -rg 'assert\.Len\(t, metrics' --type go internal/ -grep -n "assert.Len" internal/libvirt/*_test.go -``` - -**Signal**: -```go -assert.Len(t, metrics, 3, "expected exactly 3 metrics") -``` - -**Why this matters**: Adding a label to an existing metric (e.g., adding `numa_node` label to steal time) changes the cardinality. The exact-count assertion breaks, but the metric is still correct. Tests become a maintenance burden. - -**Preferred action**: Assert metric presence and specific label values rather than total count: -```go -assert.NotEmpty(t, metrics) -// Find the specific metric you care about: -found := false -for _, m := range metrics { - if strings.Contains(m.Desc().String(), "kvm_domain_libvirt_steal_time") { - found = true - } -} -assert.True(t, found, "steal time metric must be present") -``` - ---- - -### Add test-metrics.sh Coverage for Every New Collector - -**Detection**: -```bash -grep -n "check_metric" test/test-metrics.sh -# Compare against collector list in DISABLED_COLLECTORS docs -``` -If a new collector name does not appear in `test-metrics.sh`, its E2E coverage is missing. - -**Signal**: -New collector `hugepages` is added to `internal/libvirt/hugepages.go` but `test/test-metrics.sh` has no `check_metric "kvm_domain_hugepages_bytes"` line. - -**Why this matters**: Unit tests cover the mock path. The E2E cluster deploys the exporter against real VMs. If hugepages collection silently returns no metrics (e.g., smaps not readable), no test catches it. - -**Preferred action**: For every new collector, add at minimum one `check_metric "kvm_..."` line to `test/test-metrics.sh` targeting the primary metric the collector emits. - ---- - -## Error-Fix Mappings - -| Error | Root Cause | Fix | -|-------|-----------|-----| -| `undefined: LibVirtMock` | `interface_mock_gen.go` not regenerated after interface change | Run `make generate` | -| `DATA RACE` in `go test -race` | Goroutine-per-domain accessing shared state without sync | Check `sync.Map` usage; add mutex if using plain map | -| `no such file or directory: /run/libvirt/libvirt-sock-ro` (unit test) | Test code directly instantiating real libvirt connection | Inject mock via interface; never connect to real libvirt in unit tests | -| E2E `make test-all` fails at Kind node image build | Custom Kind Dockerfile outdated after Ubuntu/libvirt version bump | Rebuild base image: `make build-test-image` | -| `check_metric` fails in `test-metrics.sh` | New collector not returning metrics in E2E cluster | Check collector enabled/disabled status; add debug logging; validate cgroup paths on Kind node | - ---- - -## Detection Commands Reference - -```bash -# Check if race detector is in Makefile test targets -grep -n "go test" Makefile - -# Find any test that skips -race -rg 'go test [^-]' Makefile - -# Find hand-edits to generated mock file -git log --oneline internal/libvirt/interface_mock_gen.go - -# Find exact-count metric assertions (fragile) -rg 'assert\.Len\(t, metrics' --type go internal/ - -# Find uncovered collectors in E2E script -grep "check_metric" test/test-metrics.sh - -# Run unit tests with race detector -go test -race -v ./internal/... -``` - ---- - -## See Also - -- `references/kvm-exporter.md` — Testing section for E2E infrastructure setup and `make test-all` targets -- `references/concurrency-patterns.md` — Concurrency patterns that need race-detector-clean tests diff --git a/skills/engineering/kotlin-coroutines/SKILL.md b/skills/engineering/kotlin-coroutines/SKILL.md deleted file mode 100644 index 3f489c2b8..000000000 --- a/skills/engineering/kotlin-coroutines/SKILL.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -name: kotlin-coroutines -promoted_to: kotlin -description: "Kotlin structured concurrency, Flow, and Channel patterns." -user-invocable: false -context: fork -agent: kotlin-general-engineer -routing: - triggers: - - "kotlin coroutines" - - "kotlin structured concurrency" - - "kotlin Flow" - - "kotlin Channel" - - "suspend function" - - "structured concurrency kotlin" - category: kotlin - pairs_with: - - kotlin-testing ---- - -# Kotlin Coroutines Patterns - -Umbrella skill for Kotlin coroutine development: structured concurrency, cancellation, -Flow, StateFlow/SharedFlow, Channels, exception handling, and dispatchers. Routes to -the correct reference based on the task at hand. - -## Reference Loading Table - -| Signal | Load These Files | Why | -|---|---|---| -| Concurrency | `concurrency-patterns.md` | Scopes, cancellation, dispatchers, exception handling | -| Flow | `flow-patterns.md` | Flow builders, StateFlow, SharedFlow, operators | -| Channels | `channel-patterns.md` | Producer-consumer, fan-in/fan-out patterns | -| Failure modes | `preferred-patterns.md` | GlobalScope, unstructured launch, CancellationException | - -## Instructions - -### Step 1: Identify the Domain - -Classify the task into one or more domains, then load the corresponding reference files. -Only load what is needed -- do not load all references for every task. - -| Domain | Load Reference | When | -|--------|---------------|------| -| Concurrency | `references/concurrency-patterns.md` | Scopes, cancellation, dispatchers, exception handling | -| Flow | `references/flow-patterns.md` | Flow builders, StateFlow, SharedFlow, operators | -| Channels | `references/channel-patterns.md` | Producer-consumer, fan-in/fan-out patterns | -| Failure modes | `references/preferred-patterns.md` | GlobalScope, unstructured launch, CancellationException | - -Multiple domains may apply. For example, reviewing code that uses both Flow and Channels -should load both `flow-patterns.md` and `channel-patterns.md`. - -### Step 2: Load and Follow the Reference - -Read the selected reference file(s) using `${CLAUDE_SKILL_DIR}/references/.md`. -Each reference contains the full patterns, code examples, and decision matrices for that -domain. Follow the instructions in the reference as if they were this skill's instructions. - -### Step 3: Execute - -Apply the loaded reference patterns to the task. Use the code examples as templates -for implementation guidance. - -## Key Principles - -1. **Structured concurrency is non-negotiable** -- every coroutine must have a parent scope that defines its lifetime. -2. **Inject dispatchers** -- accept `CoroutineDispatcher` as a parameter so callers (and tests) can control threading. -3. **Always rethrow CancellationException** -- rethrow it immediately or use specific exception types instead of catching `Exception`. -4. **Prefer Flow over Channel** -- Flow is cold, composable, and handles backpressure. Channels are lower-level; reach for them only when Flow cannot express the pattern. -5. **Use supervisorScope for partial failure tolerance** -- when independent tasks should not cancel each other, wrap them in supervisorScope. -6. **Use scoped coroutines instead of GlobalScope** -- it has no lifecycle, no cancellation, and no structured concurrency. Pass a scope from your application framework instead. diff --git a/skills/engineering/kotlin-coroutines/references/channel-patterns.md b/skills/engineering/kotlin-coroutines/references/channel-patterns.md deleted file mode 100644 index dd31ebb32..000000000 --- a/skills/engineering/kotlin-coroutines/references/channel-patterns.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -description: "Kotlin Channel types, fan-in/fan-out, and producer-consumer patterns." -level: 2 ---- - -# Kotlin Channel Patterns - -## Channels: Hot Communication Primitives - -Channels are hot -- they exist independently of consumers. Use them for producer-consumer patterns and inter-coroutine communication. - -```kotlin -import kotlinx.coroutines.channels.* - -// Producer-consumer with produce builder -fun CoroutineScope.produceNumbers(): ReceiveChannel = produce { - var n = 1 - while (true) { - send(n++) - delay(100) - } -} - -// Fan-out: multiple coroutines consuming from one channel -suspend fun fanOut() = coroutineScope { - val channel = produceNumbers() - - repeat(3) { workerId -> - launch { - for (number in channel) { - println("Worker $workerId processing $number") - } - } - } -} - -// Fan-in: multiple producers writing to one channel -suspend fun fanIn() = coroutineScope { - val channel = Channel() - - launch { repeat(5) { channel.send("Source A: $it"); delay(100) } } - launch { repeat(5) { channel.send("Source B: $it"); delay(150) } } - - // Collect all from both sources - repeat(10) { - println(channel.receive()) - } - channel.close() -} -``` diff --git a/skills/engineering/kotlin-coroutines/references/concurrency-patterns.md b/skills/engineering/kotlin-coroutines/references/concurrency-patterns.md deleted file mode 100644 index 62ce14638..000000000 --- a/skills/engineering/kotlin-coroutines/references/concurrency-patterns.md +++ /dev/null @@ -1,181 +0,0 @@ ---- -description: "Kotlin structured concurrency: coroutineScope, supervisorScope, cancellation, dispatchers, exception handling." -level: 2 ---- - -# Kotlin Concurrency Patterns - -## Structured Concurrency - -Every coroutine must belong to a scope. The scope defines the lifetime -- when the scope is cancelled, all its children are cancelled. Tie every coroutine to a scope. - -```kotlin -import kotlinx.coroutines.* - -// coroutineScope suspends until ALL children complete. -// If any child fails, all siblings are cancelled. -suspend fun fetchDashboard(): Dashboard = coroutineScope { - val user = async { userService.getUser() } - val orders = async { orderService.getRecent() } - val notifications = async { notificationService.getUnread() } - - Dashboard( - user = user.await(), - orders = orders.await(), - notifications = notifications.await() - ) -} - -// supervisorScope lets children fail independently. -// One child's failure does NOT cancel siblings. -suspend fun refreshCaches(): List> = supervisorScope { - val jobs = listOf("users", "products", "inventory").map { cache -> - async { - runCatching { cacheService.refresh(cache) } - } - } - jobs.awaitAll() -} -``` - -### coroutineScope vs supervisorScope - -| Behavior | `coroutineScope` | `supervisorScope` | -|----------|------------------|-------------------| -| Child failure | Cancels all siblings | Siblings continue | -| Use when | All results required | Partial success acceptable | -| Exception | Rethrown from scope | Must handle per-child | - -## Cancellation - -Coroutines use cooperative cancellation. Long-running work must check `isActive` or call suspending functions that respect cancellation. - -```kotlin -suspend fun processLargeFile(file: File) = coroutineScope { - file.useLines { lines -> - lines.forEach { line -> - // Check cancellation between iterations - ensureActive() - processLine(line) - } - } -} - -// CPU-bound loops MUST check isActive — they won't suspend naturally -suspend fun computeHash(data: ByteArray): String = withContext(Dispatchers.Default) { - var hash = 0L - for (i in data.indices) { - if (!isActive) break // Respect cancellation - hash = hash * 31 + data[i] - } - hash.toString(16) -} -``` - -### NonCancellable for Cleanup - -When you need to run suspending cleanup code after cancellation, use `NonCancellable`. - -```kotlin -suspend fun transferFunds(from: Account, to: Account, amount: BigDecimal) { - try { - from.debit(amount) - to.credit(amount) - } finally { - // After cancellation, the coroutine is in a "cancelling" state. - // Suspending calls would throw CancellationException — unless - // we switch to NonCancellable. - withContext(NonCancellable) { - auditLog.record("Transfer attempted: $amount from ${from.id} to ${to.id}") - } - } -} -``` - -## Exception Handling - -Exceptions in coroutines propagate up the scope hierarchy. Uncaught exceptions cancel the parent scope. - -```kotlin -// CoroutineExceptionHandler — last resort for uncaught exceptions in launch. -// Only works with launch (NOT async). Only at root scope level. -val handler = CoroutineExceptionHandler { _, exception -> - logger.error("Uncaught coroutine exception", exception) - metrics.incrementCounter("coroutine.unhandled_exception") -} - -val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default + handler) - -scope.launch { - // If this throws, handler catches it instead of crashing - riskyOperation() -} - -// For async, exceptions surface at await() -val deferred = scope.async { - riskyOperation() // Exception stored, not thrown yet -} -try { - deferred.await() // Exception thrown HERE -} catch (e: Exception) { - handleError(e) -} -``` - -### try-catch in Coroutines - -```kotlin -// Wrap individual operations, not the entire coroutine -suspend fun fetchWithFallback(): Data { - return try { - remoteApi.fetch() - } catch (e: IOException) { - logger.warn("Remote fetch failed, using cache", e) - localCache.get() - } -} - -// Always rethrow CancellationException — it breaks structured concurrency -suspend fun badExample() { - try { - someWork() - } catch (e: Exception) { - // Before: This catches CancellationException too! - // The coroutine won't cancel properly. - } -} - -suspend fun goodExample() { - try { - someWork() - } catch (e: CancellationException) { - throw e // Always rethrow - } catch (e: Exception) { - handleError(e) - } -} -``` - -## Dispatchers - -| Dispatcher | Thread Pool | Use For | -|------------|------------|---------| -| `Dispatchers.Default` | Shared, sized to CPU cores | CPU-bound work (parsing, sorting) | -| `Dispatchers.IO` | Elastic, up to 64 threads | Blocking I/O (file, network, JDBC) | -| `Dispatchers.Main` | Single UI thread | UI updates (Android, Swing) | -| `Dispatchers.Unconfined` | Caller's thread (until first suspend) | Testing only; avoid in production | - -```kotlin -// Switch dispatcher for blocking calls -suspend fun readConfig(): Config = withContext(Dispatchers.IO) { - val text = File("config.json").readText() // Blocking call on IO pool - Json.decodeFromString(text) -} - -// Custom dispatcher for limited parallelism -val dbDispatcher = Dispatchers.IO.limitedParallelism(4) - -suspend fun queryDatabase(): List = withContext(dbDispatcher) { - connection.executeQuery("SELECT * FROM orders") -} -``` diff --git a/skills/engineering/kotlin-coroutines/references/flow-patterns.md b/skills/engineering/kotlin-coroutines/references/flow-patterns.md deleted file mode 100644 index 99b8461c4..000000000 --- a/skills/engineering/kotlin-coroutines/references/flow-patterns.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -description: "Kotlin Flow patterns: cold streams, StateFlow, SharedFlow comparison and usage." -level: 2 ---- - -# Kotlin Flow Patterns - -## Flow: Cold Asynchronous Streams - -Flow is cold -- it does not produce values until collected. Each collector gets its own execution of the flow body. - -```kotlin -import kotlinx.coroutines.flow.* - -// Producing a flow -fun searchResults(query: String): Flow = flow { - val page1 = api.search(query, page = 1) - emit(page1) - - if (page1.hasMore) { - val page2 = api.search(query, page = 2) - emit(page2) - } -} - -// Operators: transform, filter, combine -fun processedResults(query: String): Flow = - searchResults(query) - .filter { it.results.isNotEmpty() } - .map { page -> page.results.map { it.toDisplayItem() } } - .flatMapConcat { items -> items.asFlow() } - .onEach { item -> analytics.trackImpression(item.id) } - -// Terminal operator — triggers collection -suspend fun displayResults(query: String) { - processedResults(query).collect { item -> - ui.render(item) - } -} -``` - -## StateFlow vs SharedFlow - -```kotlin -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.channels.BufferOverflow - -// StateFlow: holds a SINGLE current value, replays latest to new collectors. -// Use for UI state, configuration, or anything with a "current" value. -class CounterViewModel : ViewModel() { - private val _count = MutableStateFlow(0) - val count = _count.asStateFlow() - - fun increment() { - _count.value++ - } -} - -// SharedFlow: event stream with configurable replay. -// Use for one-shot events (navigation, toasts, errors). -class EventBus { - private val _events = MutableSharedFlow( - replay = 0, // Skip replaying old events to new subscribers - extraBufferCapacity = 64, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val events = _events.asSharedFlow() - - suspend fun emit(event: AppEvent) { - _events.emit(event) - } -} -``` - -### StateFlow vs SharedFlow Decision Matrix - -| Property | `StateFlow` | `SharedFlow` | -|----------|-------------|--------------| -| Initial value | Required | Not required | -| Replay | Always 1 (latest) | Configurable (0..N) | -| Equality | Conflates duplicate values | Emits all values | -| Use case | Current state | Event streams | diff --git a/skills/engineering/kotlin-coroutines/references/preferred-patterns.md b/skills/engineering/kotlin-coroutines/references/preferred-patterns.md deleted file mode 100644 index 5ba2668cf..000000000 --- a/skills/engineering/kotlin-coroutines/references/preferred-patterns.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -description: "Kotlin coroutine failure modes: GlobalScope, unstructured launch, CancellationException swallowing." -level: 2 ---- - -# Kotlin Coroutine Failure Patterns - -## GlobalScope: Fire-and-Forget Leak - -```kotlin -// Before: No lifecycle management, lives until process dies -fun handleRequest(request: Request) { - GlobalScope.launch { - auditService.log(request) // If this hangs, it leaks forever - } -} - -// After: Use a scoped coroutine tied to the component lifecycle -class RequestHandler(private val scope: CoroutineScope) { - fun handleRequest(request: Request) { - scope.launch { - auditService.log(request) - } - } -} -``` - -GlobalScope has no lifecycle, no cancellation, and no structured concurrency. Pass a scope from your application framework instead. - -## Unstructured launch Without Join - -```kotlin -// After: coroutineScope waits for all children -suspend fun processAll(items: List) = coroutineScope { - items.forEach { item -> - launch { process(item) } // These run concurrently - } - // coroutineScope suspends until all children complete -} - -// Before: Using a detached scope means no waiting -fun processAllBroken(items: List) { - val scope = CoroutineScope(Dispatchers.Default) - items.forEach { item -> - scope.launch { process(item) } // No one awaits these! - } - // Function returns immediately, work may remain incomplete -} -``` - -## Catching CancellationException - -```kotlin -// Before: Swallowing cancellation breaks the entire coroutine tree -try { - longRunningWork() -} catch (e: Exception) { /* swallows CancellationException */ } - -// After: Explicit rethrow -try { - longRunningWork() -} catch (e: CancellationException) { - throw e -} catch (e: Exception) { - recover(e) -} -``` - -Always rethrow CancellationException. Catching `Exception` broadly swallows the cancellation signal, preventing the coroutine tree from shutting down properly. diff --git a/skills/engineering/kotlin-testing/SKILL.md b/skills/engineering/kotlin-testing/SKILL.md deleted file mode 100644 index 0fc543207..000000000 --- a/skills/engineering/kotlin-testing/SKILL.md +++ /dev/null @@ -1,291 +0,0 @@ ---- -name: kotlin-testing -promoted_to: kotlin -description: "Kotlin testing with JUnit 5, Kotest, and coroutine dispatchers." -user-invocable: false -context: fork -agent: kotlin-general-engineer -routing: - triggers: - - "kotlin testing" - - "junit kotlin" - - "kotest" - - "junit 5 kotlin" - - "kotlin test dispatcher" - category: kotlin - pairs_with: - - kotlin-coroutines - - test-driven-development ---- - -# Kotlin Testing Patterns - -## JUnit 5 Fundamentals - -Use `@Test` for simple test cases. Prefer `@DisplayName` for readable test names. - -```kotlin -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.DisplayName -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -class UserServiceTest { - - @Test - @DisplayName("should return user by ID when user exists") - fun findUserById() { - val service = UserService(FakeUserRepository()) - val user = service.findById(1L) - assertEquals("Alice", user.name) - } - - @Test - fun `should throw when user not found`() { - val service = UserService(FakeUserRepository()) - assertFailsWith { - service.findById(999L) - } - } -} -``` - -## Parameterized Tests - -Use `@ParameterizedTest` with `@MethodSource` for complex inputs or `@CsvSource` for simple value pairs. - -```kotlin -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.CsvSource -import org.junit.jupiter.params.provider.MethodSource -import org.junit.jupiter.params.provider.Arguments -import java.util.stream.Stream - -class ValidatorTest { - - @ParameterizedTest - @CsvSource( - "alice@example.com, true", - "not-an-email, false", - "'', false" - ) - fun `should validate email addresses`(input: String, expected: Boolean) { - assertEquals(expected, EmailValidator.isValid(input)) - } - - @ParameterizedTest - @MethodSource("provideUserInputs") - fun `should reject invalid users`(input: UserInput, expectedError: String) { - val result = UserValidator.validate(input) - assertTrue(result.isFailure) - assertEquals(expectedError, result.exceptionOrNull()?.message) - } - - companion object { - @JvmStatic - fun provideUserInputs(): Stream = Stream.of( - Arguments.of(UserInput(name = "", age = 25), "Name must not be blank"), - Arguments.of(UserInput(name = "Bob", age = -1), "Age must be positive"), - ) - } -} -``` - -## Table-Driven Tests - -Kotlin's data classes and list literals make table-driven tests natural without frameworks. - -```kotlin -@Test -fun `should parse duration strings correctly`() { - data class Case(val input: String, val expectedMs: Long, val label: String) - - val cases = listOf( - Case("100ms", 100, "milliseconds"), - Case("5s", 5000, "seconds"), - Case("2m", 120_000, "minutes"), - Case("1h", 3_600_000, "hours"), - ) - - cases.forEach { (input, expectedMs, label) -> - val result = DurationParser.parse(input) - assertEquals(expectedMs, result, "Failed for $label: input=$input") - } -} -``` - -## Kotest Styles - -Kotest offers multiple spec styles. Pick one per project for consistency. - -```kotlin -import io.kotest.core.spec.style.FunSpec -import io.kotest.core.spec.style.BehaviorSpec -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import io.kotest.assertions.throwables.shouldThrow - -// FunSpec — closest to JUnit, good default choice -class CalculatorFunSpec : FunSpec({ - test("addition of two numbers") { - Calculator.add(2, 3) shouldBe 5 - } - - context("division") { - test("divides evenly") { - Calculator.divide(10, 2) shouldBe 5 - } - - test("throws on divide by zero") { - shouldThrow { - Calculator.divide(1, 0) - } - } - } -}) - -// BehaviorSpec — Given/When/Then for acceptance-style tests -class OrderBehaviorSpec : BehaviorSpec({ - Given("a cart with two items") { - val cart = Cart().apply { - add(Item("Widget", 9.99)) - add(Item("Gadget", 19.99)) - } - - When("checkout is completed") { - val order = cart.checkout() - - Then("order total reflects both items") { - order.total shouldBe 29.98 - } - - Then("order contains two line items") { - order.items.size shouldBe 2 - } - } - } -}) - -// StringSpec — minimal boilerplate for simple tests -class StringSpecExample : StringSpec({ - "length of hello should be 5" { - "hello".length shouldBe 5 - } - - "trimmed string should not contain leading spaces" { - " padded ".trim() shouldBe "padded" - } -}) -``` - -## Kotest Matchers - -Kotest provides expressive matchers beyond `shouldBe`. - -```kotlin -import io.kotest.matchers.collections.shouldContainExactly -import io.kotest.matchers.collections.shouldHaveSize -import io.kotest.matchers.string.shouldStartWith -import io.kotest.matchers.nulls.shouldNotBeNull - -val names = listOf("Alice", "Bob", "Charlie") -names shouldHaveSize 3 -names shouldContainExactly listOf("Alice", "Bob", "Charlie") - -val greeting: String? = getGreeting() -greeting.shouldNotBeNull() -greeting shouldStartWith "Hello" -``` - -## Coroutine Testing - -Use `runTest` from `kotlinx-coroutines-test` to test suspending functions. `runTest` auto-advances virtual time. - -```kotlin -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle - -class NotificationServiceTest { - - private val testDispatcher = StandardTestDispatcher() - - @Test - fun `should send notification after delay`() = runTest(testDispatcher) { - val service = NotificationService(dispatcher = testDispatcher) - - service.scheduleNotification("Hello", delayMs = 5000) - advanceUntilIdle() - - assertEquals(1, service.sentCount) - } - - @Test - fun `should cancel pending notifications`() = runTest(testDispatcher) { - val service = NotificationService(dispatcher = testDispatcher) - val job = service.scheduleNotification("Hello", delayMs = 5000) - - job.cancel() - advanceUntilIdle() - - assertEquals(0, service.sentCount) - } -} -``` - -## MockK - -MockK is the idiomatic Kotlin mocking library. Use `every {}` for stubs, `verify {}` for assertions, and `coEvery {}` / `coVerify {}` for suspending functions. - -```kotlin -import io.mockk.mockk -import io.mockk.every -import io.mockk.coEvery -import io.mockk.verify -import io.mockk.coVerify -import io.mockk.slot - -class OrderServiceTest { - - private val repo = mockk() - private val notifier = mockk(relaxed = true) - private val service = OrderService(repo, notifier) - - @Test - fun `should save order and notify`() { - val orderSlot = slot() - every { repo.save(capture(orderSlot)) } returns Order(id = 42) - - service.placeOrder(items = listOf("Widget")) - - assertEquals(42, orderSlot.captured.id) - verify(exactly = 1) { notifier.send(any()) } - } - - @Test - fun `should fetch order from remote API`() = runTest { - coEvery { repo.fetchRemote(1L) } returns Order(id = 1, status = "shipped") - - val order = service.getOrder(1L) - - assertEquals("shipped", order.status) - coVerify { repo.fetchRemote(1L) } - } -} -``` - -## Assertion Libraries Summary - -| Library | Syntax Style | Best For | -|---------|-------------|----------| -| `kotlin.test` | `assertEquals(expected, actual)` | JUnit 5 projects, zero extra deps | -| Kotest matchers | `actual shouldBe expected` | Readable assertions, rich matcher library | -| AssertJ (via assertk) | `assertThat(actual).isEqualTo(expected)` | Java interop, fluent chains | - -## Key Principles - -1. **One assertion concept per test** -- a test should verify one behavior, though it may need multiple assertions to do so. -2. **Use `runTest` for all coroutine tests** -- never use `runBlocking` in tests; `runTest` handles virtual time and uncaught exceptions. -3. **Prefer fakes over mocks** -- mocks couple tests to implementation; fakes (manual implementations) couple tests to contracts. -4. **Name tests as behavior specifications** -- backtick names like `` `should return empty list when no results` `` read better in reports. -5. **Inject dispatchers** -- never hardcode `Dispatchers.IO` in production code; accept a `CoroutineDispatcher` parameter so tests can supply `TestDispatcher`. diff --git a/skills/engineering/php-quality/SKILL.md b/skills/engineering/php-quality/SKILL.md deleted file mode 100644 index 6314f0eec..000000000 --- a/skills/engineering/php-quality/SKILL.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -name: php-quality -promoted_to: php -description: "PHP code quality: PSR standards, strict types, framework idioms." -user-invocable: false -context: fork -agent: php-general-engineer -routing: - triggers: - - "php quality" - - "php code review" - - "PSR standards" - - "phpstan" - - "php-cs-fixer" - category: php - pairs_with: - - php-testing - - code-linting ---- - -# PHP Quality Skill - -PHP code quality enforcement: strict types, PSR-12 compliance, modern language features, framework idioms, and static analysis tooling. - -## Reference Loading Table - -| Signal | Reference | Size | -|--------|-----------|------| -| union types, intersection types, DNF, enums, readonly, named arguments, match expression, null-safe operator, PHP 8.0, PHP 8.1, PHP 8.2 | `references/modern-php-features.md` | ~160 lines | -| Laravel, Eloquent, Collections, Service Container, Symfony, DI attributes, Event Dispatcher, framework | `references/framework-idioms.md` | ~70 lines | -| PHP-CS-Fixer, PHPStan, Psalm, Rector, static analysis, CI, linting, code style, taint analysis | `references/quality-tools.md` | ~60 lines | - -**Loading rule.** Read the references whose signals match the task before responding. - ---- - -## Core Rules (Always Apply) - -### Strict Types Declaration - -Every PHP file must begin with `declare(strict_types=1)`. This enforces scalar type coercion rules, catching type errors at call time instead of silently converting values. - -```php - **Scope**: Idiomatic patterns for Laravel and Symfony. Covers Eloquent scopes, Collections, Service Container binding, Symfony DI attributes, and Event Dispatcher. Does NOT cover raw PHP patterns without a framework or quality tooling configuration. -> **Version range**: Laravel 10+, Symfony 6.2+ -> **Generated**: 2026-04-16 - ---- - -## Laravel Idioms - -### Eloquent - -```php -// Scopes for reusable query constraints -class User extends Model -{ - public function scopeActive(Builder $query): Builder - { - return $query->where('active', true); - } -} - -// Usage: User::active()->where('role', 'admin')->get(); -``` - -### Collections - -```php -// Prefer collection methods over raw loops -$names = collect($users) - ->filter(fn (User $u) => $u->isActive()) - ->map(fn (User $u) => $u->fullName()) - ->sort() - ->values() - ->all(); -``` - -### Service Container - -```php -// Bind interface to implementation in a ServiceProvider -$this->app->bind(PaymentGateway::class, StripeGateway::class); - -// Contextual binding -$this->app->when(OrderService::class) - ->needs(PaymentGateway::class) - ->give(StripeGateway::class); -``` - ---- - -## Symfony Idioms - -### Dependency Injection with Attributes - -```php -use Symfony\Component\DependencyInjection\Attribute\Autowire; - -class ReportGenerator -{ - public function __construct( - private readonly ReportRepository $repository, - #[Autowire('%kernel.project_dir%/var/reports')] - private readonly string $outputDir, - ) { - } -} -``` - -### Event Dispatcher - -```php -use Symfony\Component\EventDispatcher\Attribute\AsEventListener; - -#[AsEventListener(event: OrderPlaced::class)] -class SendOrderConfirmation -{ - public function __invoke(OrderPlaced $event): void - { - // Send confirmation email for $event->orderId - } -} -``` diff --git a/skills/engineering/php-quality/references/modern-php-features.md b/skills/engineering/php-quality/references/modern-php-features.md deleted file mode 100644 index 33f77be57..000000000 --- a/skills/engineering/php-quality/references/modern-php-features.md +++ /dev/null @@ -1,197 +0,0 @@ -# Modern PHP Features Reference - -> **Scope**: PHP 8.0, 8.1, and 8.2 language features relevant to code quality reviews: type system improvements, enums, readonly, match expressions, named arguments, null-safe operator. Does NOT cover framework-specific patterns or tooling configuration. -> **Version range**: PHP 8.0 through 8.2 -> **Generated**: 2026-04-16 - ---- - -## Type Declarations - -### Union Types (PHP 8.0) - -```php -function parseId(int|string $id): User -{ - return match (true) { - is_int($id) => $this->findById($id), - is_string($id) => $this->findBySlug($id), - }; -} -``` - -### Intersection Types (PHP 8.1) - -```php -function processItem(Countable&Iterator $collection): void -{ - // $collection must implement BOTH interfaces - foreach ($collection as $item) { - // ... - } -} -``` - -### DNF Types (PHP 8.2) - -Disjunctive Normal Form combines union and intersection types. - -```php -function handle((Countable&Iterator)|null $items): void -{ - // Combines union and intersection: (A&B)|C - if ($items === null) { - return; - } - // ... -} -``` - ---- - -## Enums (PHP 8.1) - -### Backed Enums - -```php -enum Status: string -{ - case Active = 'active'; - case Inactive = 'inactive'; - case Pending = 'pending'; - - // Enums can have methods - public function label(): string - { - return match ($this) { - self::Active => 'Active', - self::Inactive => 'Inactive', - self::Pending => 'Pending Review', - }; - } - - // And implement interfaces - public function isTerminal(): bool - { - return $this === self::Inactive; - } -} - -// Usage -$status = Status::from('active'); // throws ValueError if invalid -$status = Status::tryFrom('unknown'); // returns null if invalid -$value = Status::Active->value; // 'active' -``` - -Backed enums can be `string` or `int`. Use them instead of class constants for fixed value sets. - ---- - -## Readonly Properties and Classes (PHP 8.1 / 8.2) - -```php -// Readonly properties (PHP 8.1) — set once, immutable after -class Money -{ - public function __construct( - public readonly int $amount, - public readonly string $currency, - ) { - } -} - -// Readonly classes (PHP 8.2) — all properties are implicitly readonly -readonly class Coordinate -{ - public function __construct( - public float $latitude, - public float $longitude, - ) { - } -} - -$c = new Coordinate(37.7749, -122.4194); -// $c->latitude = 0; // Error: Cannot modify readonly property -``` - ---- - -## Named Arguments (PHP 8.0) - -Named arguments improve readability for functions with many parameters or boolean flags. - -```php -// Before: positional arguments — what does true mean? -$user = createUser('Alice', 'alice@example.com', true, false); - -// After: named arguments — intent is clear -$user = createUser( - name: 'Alice', - email: 'alice@example.com', - isAdmin: true, - sendWelcomeEmail: false, -); - -// Named arguments can skip optional parameters -htmlspecialchars($string, encoding: 'UTF-8'); -``` - ---- - -## Match Expressions (PHP 8.0) - -`match` is a stricter alternative to `switch`. It uses strict comparison (`===`), returns a value, and throws `UnhandledMatchError` for missing cases. - -```php -// switch — loose comparison, fall-through risk, verbose -switch ($statusCode) { - case 200: - $text = 'OK'; - break; - case 404: - $text = 'Not Found'; - break; - default: - $text = 'Unknown'; -} - -// match — strict comparison, no fall-through, expression -$text = match ($statusCode) { - 200 => 'OK', - 301 => 'Moved Permanently', - 404 => 'Not Found', - 500 => 'Internal Server Error', - default => 'Unknown', -}; - -// match with no subject — replaces if/elseif chains -$category = match (true) { - $age < 13 => 'child', - $age < 18 => 'teen', - $age < 65 => 'adult', - default => 'senior', -}; -``` - ---- - -## Null Safe Operator (PHP 8.0) - -The `?->` operator short-circuits to `null` when the left side is null, eliminating nested null checks. - -```php -// Before: defensive null checking -$country = null; -if ($user !== null) { - $address = $user->getAddress(); - if ($address !== null) { - $country = $address->getCountry(); - } -} - -// After: nullsafe chaining -$country = $user?->getAddress()?->getCountry(); - -// Combine with null coalescing for defaults -$countryCode = $user?->getAddress()?->getCountry()?->code ?? 'US'; -``` diff --git a/skills/engineering/php-quality/references/quality-tools.md b/skills/engineering/php-quality/references/quality-tools.md deleted file mode 100644 index 4f066f1ac..000000000 --- a/skills/engineering/php-quality/references/quality-tools.md +++ /dev/null @@ -1,52 +0,0 @@ -# Quality Tools Reference - -> **Scope**: PHP code quality tool configuration and usage: PHP-CS-Fixer, PHPStan, Psalm, Rector. Covers CI integration commands, config files, and recommended strictness levels. Does NOT cover language features or framework patterns. -> **Version range**: PHP-CS-Fixer 3.x, PHPStan 1.x, Psalm 5.x, Rector 1.x -> **Generated**: 2026-04-16 - ---- - -## CI Commands - -Run these tools in CI: - -```bash -# PHP-CS-Fixer — auto-fix PSR-12 violations -./vendor/bin/php-cs-fixer fix --dry-run --diff - -# PHPStan — static analysis (level 0-9, aim for 6+) -./vendor/bin/phpstan analyse src --level=6 - -# Psalm — alternative static analysis -./vendor/bin/psalm --show-info=true - -# Rector — automated refactoring and upgrades -./vendor/bin/rector process src --dry-run -``` - ---- - -## Tool Reference - -| Tool | Purpose | Config File | -|------|---------|-------------| -| PHP-CS-Fixer | Code style enforcement | `.php-cs-fixer.dist.php` | -| PHPStan | Static analysis, type checking | `phpstan.neon` | -| Psalm | Static analysis, taint analysis | `psalm.xml` | -| Rector | Automated refactoring | `rector.php` | - -### PHP-CS-Fixer - -Enforces PSR-12 and custom code style rules. Config file `.php-cs-fixer.dist.php` defines rulesets. Use `--dry-run --diff` in CI to detect violations without modifying files. Run `fix` locally to auto-correct. - -### PHPStan - -Static analysis with levels 0 through 9. Level 6 is the recommended minimum for production code: it covers method return types, property types, and dead code detection. Higher levels add strictness around mixed types and dynamic property access. - -### Psalm - -Alternative to PHPStan with taint analysis for security-sensitive code paths. Shows data flow from user input to dangerous sinks (SQL queries, shell commands, file operations). Use `--show-info=true` to surface non-error findings like unused variables. - -### Rector - -Automated refactoring engine. Upgrades PHP syntax to target version (e.g., converting `switch` to `match`, adding property promotion). Always run with `--dry-run` first in CI to preview changes. diff --git a/skills/engineering/php-testing/SKILL.md b/skills/engineering/php-testing/SKILL.md deleted file mode 100644 index fe1cf6eae..000000000 --- a/skills/engineering/php-testing/SKILL.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -name: php-testing -promoted_to: php -description: "PHP testing patterns: PHPUnit, test doubles, database testing." -user-invocable: false -context: fork -agent: php-general-engineer -routing: - triggers: - - "php testing" - - "phpunit" - - "pest php" - - "php mock" - category: php - pairs_with: - - php-quality - - test-driven-development ---- - -# PHP Testing Skill - -## Overview - -Apply PHPUnit testing patterns for PHP projects: unit tests with data providers, test doubles (stubs, mocks, Prophecy), database testing (Laravel/Symfony), HTTP testing, and coverage configuration. - -> See `references/patterns.md` for full code examples, the failure modes table, and the commands reference. - -## Reference Loading Table - -| Signal | Load These Files | Why | -|---|---|---| -| writing PHPUnit tests: data providers, mocks/stubs, database or HTTP tests, coverage config | `patterns.md` | Loads detailed guidance from `patterns.md`. | - -## Instructions - -### Phase 1: IDENTIFY - -Determine what needs testing: -- Unit logic: use PHPUnit `TestCase` with `test` prefix or `@test` annotation -- Table-driven cases: use `@dataProvider` static methods -- Collaborator behavior: use stubs (return values only) or mocks (assert interactions) -- Database state: use `DatabaseTransactions` (Laravel) or `KernelTestCase` (Symfony) -- HTTP endpoints: use Laravel HTTP helpers or Symfony `WebTestCase` - -### Phase 2: WRITE - -Write tests following these rules: -- Call `parent::setUp()` first in every `setUp()` method -- Use `assertSame()` instead of `assertTrue($a === $b)` for meaningful failure messages -- Mock only collaborators and dependencies, never the class under test -- Keep tests independent -- do not use `@depends` chains -- Extract repetitive cases to `@dataProvider` rather than duplicating test methods - -For test doubles: use `createStub()` when you only need return values, `createMock()` when asserting method calls, and Prophecy (`phpspec/prophecy-phpunit`) for more expressive interaction assertions. - -For database tests: use `DatabaseTransactions` (Laravel) or DoctrineTestBundle (Symfony) to roll back state after each test. - -### Phase 3: VERIFY - -Run the test suite and confirm all tests pass: - -```bash -./vendor/bin/phpunit -``` - -For coverage enforcement: - -```bash -XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-text --coverage-min=80 -``` - -**GATE**: All tests pass. Coverage threshold met if configured. No failure modes from `references/patterns.md` introduced. diff --git a/skills/engineering/php-testing/references/patterns.md b/skills/engineering/php-testing/references/patterns.md deleted file mode 100644 index e19b8158d..000000000 --- a/skills/engineering/php-testing/references/patterns.md +++ /dev/null @@ -1,301 +0,0 @@ -# PHP Testing Patterns Reference - -Full code examples, failure modes, and commands reference for the PHP Testing skill. - ---- - -## PHPUnit Basics - -PHPUnit is the standard testing framework for PHP. Tests extend `TestCase` and use either the `test` method prefix or the `@test` annotation. - -```php -assertSame(108.00, $invoice->total()); - } - - // Option 2: @test annotation - /** @test */ - public function it_returns_zero_for_empty_line_items(): void - { - $invoice = new Invoice(amount: 0, taxRate: 0.08); - $this->assertSame(0.00, $invoice->total()); - } -} -``` - -Use `setUp()` and `tearDown()` for shared fixtures. Call `parent::setUp()` first. - -```php -protected function setUp(): void -{ - parent::setUp(); - $this->repository = new InMemoryUserRepository(); -} -``` - ---- - -## Data Providers for Table-Driven Tests - -Data providers enable table-driven testing, feeding multiple input/output sets to a single test method. - -```php -/** - * @dataProvider validEmailProvider - */ -public function testAcceptsValidEmails(string $email): void -{ - $this->assertTrue(EmailValidator::isValid($email)); -} - -public static function validEmailProvider(): array -{ - return [ - 'standard address' => ['user@example.com'], - 'subdomain' => ['user@mail.example.com'], - 'plus addressing' => ['user+tag@example.com'], - 'numeric domain' => ['user@123.123.123.com'], - ]; -} - -/** - * @dataProvider arithmeticProvider - */ -public function testArithmetic(int $a, int $b, int $expected): void -{ - $this->assertSame($expected, Calculator::add($a, $b)); -} - -public static function arithmeticProvider(): iterable -{ - yield 'positive numbers' => [2, 3, 5]; - yield 'negative numbers' => [-1, -2, -3]; - yield 'mixed signs' => [-1, 3, 2]; - yield 'zeros' => [0, 0, 0]; -} -``` - ---- - -## Test Doubles: Mocks, Stubs, and Prophecy - -PHPUnit provides `createMock()` and `createStub()`. Use stubs when you only need return values; use mocks when you need to assert interactions. - -```php -// Stub — configure return values, no call assertions -public function testFetchesUserFromCache(): void -{ - $cache = $this->createStub(CacheInterface::class); - $cache->method('get')->willReturn(new User('Alice')); - - $service = new UserService($cache); - $this->assertSame('Alice', $service->getUser('alice')->name); -} - -// Mock — assert the method was called with specific arguments -public function testLogsFailedPayment(): void -{ - $logger = $this->createMock(LoggerInterface::class); - $logger->expects($this->once()) - ->method('error') - ->with($this->stringContains('Payment failed')); - - $processor = new PaymentProcessor($logger); - $processor->process(new Payment(amount: -1)); -} -``` - -For more expressive test doubles, use Prophecy (bundled via `phpspec/prophecy-phpunit`): - -```php -use Prophecy\PhpUnit\ProphecyTrait; - -class OrderServiceTest extends TestCase -{ - use ProphecyTrait; - - public function testDispatchesOrderEvent(): void - { - $dispatcher = $this->prophesize(EventDispatcherInterface::class); - $dispatcher->dispatch(Argument::type(OrderPlaced::class)) - ->shouldBeCalledOnce(); - - $service = new OrderService($dispatcher->reveal()); - $service->place(new Order(id: 42)); - } -} -``` - ---- - -## Database Testing - -### Laravel: DatabaseTransactions, Factories, Seeders - -```php -use Illuminate\Foundation\Testing\DatabaseTransactions; -use Tests\TestCase; - -class UserRepositoryTest extends TestCase -{ - use DatabaseTransactions; // rolls back after each test - - public function testFindsActiveUsers(): void - { - // Factories create test data - User::factory()->count(3)->create(['active' => true]); - User::factory()->count(2)->create(['active' => false]); - - $active = $this->app->make(UserRepository::class)->findActive(); - - $this->assertCount(3, $active); - } -} -``` - -### Symfony: DAMA DoctrineTestBundle - -```php -use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; - -class ProductRepositoryTest extends KernelTestCase -{ - public function testCountsByCategoryId(): void - { - self::bootKernel(); - $em = self::getContainer()->get('doctrine')->getManager(); - - $count = $em->getRepository(Product::class) - ->countByCategory(categoryId: 5); - - $this->assertGreaterThan(0, $count); - } -} -``` - ---- - -## HTTP Testing - -### Laravel HTTP Tests - -```php -public function testCreateEndpointReturnsCreatedStatus(): void -{ - $response = $this->postJson('/api/users', [ - 'name' => 'Alice', - 'email' => 'alice@example.com', - ]); - - $response->assertStatus(201) - ->assertJsonPath('data.name', 'Alice'); - - $this->assertDatabaseHas('users', ['email' => 'alice@example.com']); -} -``` - -### Symfony WebTestCase - -```php -use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; - -class HealthCheckControllerTest extends WebTestCase -{ - public function testHealthEndpointReturnsOk(): void - { - $client = static::createClient(); - $client->request('GET', '/health'); - - $this->assertResponseIsSuccessful(); - $this->assertJsonStringEqualsJsonString( - '{"status":"ok"}', - $client->getResponse()->getContent() - ); - } -} -``` - ---- - -## Code Coverage Configuration - -Configure coverage in `phpunit.xml`: - -```xml - - - - src - - - src/Migrations - - - -``` - -Run coverage: - -```bash -# Text summary -php artisan test --coverage # Laravel -./vendor/bin/phpunit --coverage-text # Any PHP project - -# HTML report -./vendor/bin/phpunit --coverage-html coverage/ - -# Enforce minimum threshold -./vendor/bin/phpunit --coverage-text --coverage-min=80 -``` - ---- - - -## Common Testing Patterns to Fix - -| Signal | Problem | Fix | -|---|---|---| -| Testing private methods directly | Couples tests to implementation | Test through public API | -| One assertion per test (dogmatic) | Explosion of near-identical tests | Group related assertions; use data providers | -| No data providers for repetitive cases | Duplicate test methods | Extract to `@dataProvider` | -| Database state leaking between tests | Flaky, order-dependent tests | Use `DatabaseTransactions` or `setUp`/`tearDown` | -| Mocking the class under test | Test proves nothing | Mock only collaborators/dependencies | -| Ignoring `@depends` fragility | Chained tests break together | Keep tests independent; duplicate setup if needed | -| `$this->assertTrue($a === $b)` | Failure message is useless ("expected true, got false") | Use `$this->assertSame($b, $a)` for meaningful diffs | - ---- - -## Commands Reference - -```bash -# Run all tests -./vendor/bin/phpunit - -# Run a specific test class -./vendor/bin/phpunit tests/Unit/InvoiceTest.php - -# Run a specific test method -./vendor/bin/phpunit --filter testCalculatesTotalWithTax - -# Run a test suite defined in phpunit.xml -./vendor/bin/phpunit --testsuite Unit - -# Stop on first failure -./vendor/bin/phpunit --stop-on-failure - -# Run with coverage (requires Xdebug or PCOV) -XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-text -``` diff --git a/skills/engineering/php/references/php-quality.md b/skills/engineering/php/references/php-quality.md index 41edbaf59..629b0774f 100644 --- a/skills/engineering/php/references/php-quality.md +++ b/skills/engineering/php/references/php-quality.md @@ -1,5 +1,33 @@ # PHP Quality Review Process +## Core Rules (Always Apply) + +### Strict Types Declaration + +Every PHP file must begin with `declare(strict_types=1)`. This enforces scalar type coercion rules, catching type errors at call time instead of silently converting values. + +```php += amount else { - throw BankError.insufficientFunds - } - balance -= amount - } - - func getBalance() -> Decimal { - balance - } -} - -// All access is async from outside the actor -let account = BankAccount(initialBalance: 1000) -try await account.withdraw(200) -let balance = await account.getBalance() -``` - -## @MainActor - -Use `@MainActor` to confine work to the main thread, required for all UI updates. - -```swift -@MainActor -final class ProfileViewModel: ObservableObject { - @Published var user: User? - @Published var isLoading = false - - private let service: UserService - - init(service: UserService) { - self.service = service - } - - func loadProfile() async { - isLoading = true - defer { isLoading = false } - - do { - user = try await service.fetchCurrentUser() - } catch { - print("Load failed: \(error)") - } - } -} -``` - -## nonisolated - -Use `nonisolated` to opt specific methods out of actor isolation when they only read immutable state or perform no state access. - -```swift -actor CacheManager { - let maxSize: Int - private var entries: [String: Data] = [:] - - init(maxSize: Int) { - self.maxSize = maxSize - } - - // No await needed to call this -- it reads only a let property - nonisolated func description() -> String { - "CacheManager(maxSize: \(maxSize))" - } - - func store(_ data: Data, forKey key: String) { - entries[key] = data - } -} -``` diff --git a/skills/engineering/swift-concurrency/references/fundamentals.md b/skills/engineering/swift-concurrency/references/fundamentals.md deleted file mode 100644 index 3c7eeb871..000000000 --- a/skills/engineering/swift-concurrency/references/fundamentals.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -description: "async/await basics, Task and Task.detached patterns, Sendable/@Sendable protocol." -type: reference ---- - -# Swift Concurrency Fundamentals - -## async/await - -Mark functions that perform asynchronous work with `async`. Use `throws` alongside `async` for fallible operations. Call async functions with `await`. - -```swift -func fetchUser(id: String) async throws -> User { - let url = URL(string: "https://api.example.com/users/\(id)")! - let (data, response) = try await URLSession.shared.data(from: url) - - guard let httpResponse = response as? HTTPURLResponse, - httpResponse.statusCode == 200 else { - throw APIError.invalidResponse - } - - return try JSONDecoder().decode(User.self, from: data) -} - -// Calling from a synchronous context -Task { - do { - let user = try await fetchUser(id: "42") - print(user.name) - } catch { - print("Failed: \(error)") - } -} -``` - -## Task and Task.detached - -`Task { }` creates unstructured work that inherits the current actor context. `Task.detached` creates work with no inherited context -- use it sparingly. - -```swift -// Inherits current actor isolation (e.g., @MainActor) -Task { - let data = try await fetchData() - updateUI(with: data) // safe on MainActor if caller was MainActor -} - -// No inherited context -- runs on a cooperative thread pool -Task.detached(priority: .background) { - let report = await generateReport() - await MainActor.run { - displayReport(report) - } -} -``` - -## Sendable and @Sendable - -The `Sendable` protocol marks types safe to transfer across concurrency domains. The compiler enforces this at build time with strict concurrency checking. - -```swift -// Value types are implicitly Sendable -struct Coordinate: Sendable { - let latitude: Double - let longitude: Double -} - -// Classes must be final with only immutable stored properties -final class Configuration: Sendable { - let apiKey: String - let baseURL: URL - - init(apiKey: String, baseURL: URL) { - self.apiKey = apiKey - self.baseURL = baseURL - } -} - -// @Sendable closures cannot capture mutable state -func process(items: [Item], transform: @Sendable (Item) -> Result) async { - await withTaskGroup(of: Result.self) { group in - for item in items { - group.addTask { - transform(item) - } - } - } -} -``` diff --git a/skills/engineering/swift-concurrency/references/preferred-patterns.md b/skills/engineering/swift-concurrency/references/preferred-patterns.md deleted file mode 100644 index 6df1f2b6d..000000000 --- a/skills/engineering/swift-concurrency/references/preferred-patterns.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -description: "Common concurrency mistakes: blocking MainActor, task leaking, actor reentrancy." -type: reference ---- - -# Common Concurrency Pitfalls and Fixes - -## Blocking the Main Actor - -Never perform synchronous blocking work on `@MainActor`. Offload heavy computation to a detached task or a background actor. - -```swift -// BAD -- blocks the main thread -@MainActor -func loadData() { - let data = heavySyncComputation() // UI freezes - self.items = data -} - -// GOOD -- offload and return to main actor for UI update -@MainActor -func loadData() async { - let data = await Task.detached(priority: .userInitiated) { - heavySyncComputation() - }.value - self.items = data -} -``` - -## Unstructured Tasks Leaking - -Creating `Task { }` without storing or cancelling it leads to leaked work that outlives its logical scope. - -```swift -// BAD -- task leaks if view disappears -func viewDidAppear() { - Task { - while true { - await pollServer() - try await Task.sleep(for: .seconds(30)) - } - } -} - -// GOOD -- store and cancel on disappear -private var pollingTask: Task? - -func viewDidAppear() { - pollingTask = Task { - while !Task.isCancelled { - await pollServer() - try? await Task.sleep(for: .seconds(30)) - } - } -} - -func viewDidDisappear() { - pollingTask?.cancel() - pollingTask = nil -} -``` - -## Actor Reentrancy Hazard - -Awaiting inside an actor method yields the actor, allowing other callers to mutate state before your method resumes. Always re-validate state after an `await`. - -```swift -actor ImageCache { - private var cache: [URL: UIImage] = [:] - private var inFlight: [URL: Task] = [:] - - func image(for url: URL) async throws -> UIImage { - // Check cache first - if let cached = cache[url] { return cached } - - // Deduplicate in-flight requests - if let existing = inFlight[url] { - return try await existing.value - } - - let task = Task { - let (data, _) = try await URLSession.shared.data(from: url) - guard let image = UIImage(data: data) else { - throw ImageError.decodingFailed - } - return image - } - - inFlight[url] = task - - let image = try await task.value - - // Re-check: another caller may have populated cache while we awaited - cache[url] = image - inFlight[url] = nil - - return image - } -} -``` diff --git a/skills/engineering/swift-concurrency/references/task-patterns.md b/skills/engineering/swift-concurrency/references/task-patterns.md deleted file mode 100644 index ce90c15c7..000000000 --- a/skills/engineering/swift-concurrency/references/task-patterns.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -description: "TaskGroup, AsyncSequence, AsyncStream, and cancellation patterns." -type: reference ---- - -# Task Patterns - -## TaskGroup for Parallel Work - -Use `withTaskGroup` or `withThrowingTaskGroup` to fan out work and collect results. - -```swift -func fetchAllProfiles(ids: [String]) async throws -> [Profile] { - try await withThrowingTaskGroup(of: Profile.self) { group in - for id in ids { - group.addTask { - try await fetchProfile(id: id) - } - } - - var profiles: [Profile] = [] - for try await profile in group { - profiles.append(profile) - } - return profiles - } -} -``` - -### Rate-Limited Concurrency - -Limit concurrency manually when hitting external services: - -```swift -func fetchWithLimit(ids: [String], maxConcurrent: Int = 5) async throws -> [Profile] { - try await withThrowingTaskGroup(of: Profile.self) { group in - var iterator = ids.makeIterator() - var profiles: [Profile] = [] - - // Seed the group with initial batch - for _ in 0.. AsyncStream { - AsyncStream { continuation in - let delegate = LocationDelegate( - onUpdate: { location in - continuation.yield(location) - }, - onFinish: { - continuation.finish() - } - ) - // Store delegate to keep it alive - continuation.onTermination = { _ in - delegate.stopUpdating() - } - delegate.startUpdating() - } -} - -// Consuming the stream -Task { - for await location in locationUpdates() { - print("Lat: \(location.coordinate.latitude)") - } -} -``` - -## Cancellation - -Structured concurrency propagates cancellation automatically through task hierarchies. Check for cancellation in long-running work. - -```swift -func processLargeDataset(_ items: [DataItem]) async throws -> [Result] { - var results: [Result] = [] - - for item in items { - // Throws CancellationError if the task was cancelled - try Task.checkCancellation() - - let result = await process(item) - results.append(result) - } - - return results -} - -// Alternative: check without throwing -func processWithGracefulCancel(_ items: [DataItem]) async -> [Result] { - var results: [Result] = [] - - for item in items { - if Task.isCancelled { - break // stop early, return partial results - } - let result = await process(item) - results.append(result) - } - - return results -} - -// Cancelling a task -let task = Task { - try await processLargeDataset(hugeList) -} - -// Later, if the user navigates away -task.cancel() -``` diff --git a/skills/engineering/swift-testing/SKILL.md b/skills/engineering/swift-testing/SKILL.md deleted file mode 100644 index ac5956ae4..000000000 --- a/skills/engineering/swift-testing/SKILL.md +++ /dev/null @@ -1,263 +0,0 @@ ---- -name: swift-testing -promoted_to: swift -description: "Swift testing: XCTest, Swift Testing framework, async patterns." -user-invocable: false -context: fork -agent: swift-general-engineer -routing: - triggers: - - "swift testing" - - "XCTest" - - "Swift Testing framework" - - "async test swift" - not_for: "general Swift development (use swift-general-engineer agent) — this skill covers Swift testing: XCTest, Swift Testing framework, test patterns" - category: swift - pairs_with: - - swift-concurrency - - test-driven-development ---- - -# Swift Testing Patterns - -## XCTest Basics - -XCTest is Apple's foundational testing framework. Every test class inherits from `XCTestCase` and uses `setUp`/`tearDown` for lifecycle management. - -```swift -import XCTest -@testable import MyApp - -final class UserServiceTests: XCTestCase { - var sut: UserService! - var mockStore: MockUserStore! - - override func setUp() { - super.setUp() - mockStore = MockUserStore() - sut = UserService(store: mockStore) - } - - override func tearDown() { - sut = nil - mockStore = nil - super.tearDown() - } - - func testFetchUser_withValidID_returnsUser() { - mockStore.stubbedUser = User(id: "1", name: "Alice") - - let user = sut.fetchUser(id: "1") - - XCTAssertNotNil(user) - XCTAssertEqual(user?.name, "Alice") - } - - func testFetchUser_withInvalidID_returnsNil() { - mockStore.stubbedUser = nil - - let user = sut.fetchUser(id: "unknown") - - XCTAssertNil(user) - } -} -``` - -## Swift Testing Framework (Swift 5.9+) - -The Swift Testing framework replaces XCTest with a more expressive, macro-driven approach. Use `@Test` for individual tests, `#expect` for assertions, and `@Suite` for grouping. - -```swift -import Testing -@testable import MyApp - -@Suite("User Service") -struct UserServiceTests { - let mockStore = MockUserStore() - - @Test("fetches user by valid ID") - func fetchValidUser() { - mockStore.stubbedUser = User(id: "1", name: "Alice") - let service = UserService(store: mockStore) - - let user = service.fetchUser(id: "1") - - #expect(user?.name == "Alice") - } - - @Test("returns nil for unknown ID") - func fetchUnknownUser() { - mockStore.stubbedUser = nil - let service = UserService(store: mockStore) - - #expect(service.fetchUser(id: "unknown") == nil) - } -} -``` - -### Parameterized Tests - -Swift Testing supports parameterized tests natively, eliminating boilerplate for table-driven patterns. - -```swift -@Test("validates email formats", arguments: [ - ("alice@example.com", true), - ("bob@", false), - ("", false), - ("valid+tag@sub.domain.com", true), -]) -func emailValidation(email: String, isValid: Bool) { - #expect(EmailValidator.isValid(email) == isValid) -} -``` - -## Async Testing - -### Async Test Methods (XCTest) - -XCTest supports `async` test methods directly. For callback-based APIs, use `XCTestExpectation`. - -```swift -// Direct async/await support -func testFetchProfile_async() async throws { - let service = ProfileService(client: MockHTTPClient()) - - let profile = try await service.fetchProfile(userID: "1") - - XCTAssertEqual(profile.name, "Alice") -} - -// Callback-based APIs with expectations -func testFetchProfile_callback() { - let expectation = expectation(description: "Profile fetched") - let service = ProfileService(client: MockHTTPClient()) - - service.fetchProfile(userID: "1") { result in - switch result { - case .success(let profile): - XCTAssertEqual(profile.name, "Alice") - case .failure(let error): - XCTFail("Unexpected error: \(error)") - } - expectation.fulfill() - } - - waitForExpectations(timeout: 5) -} -``` - -### Async Tests with Swift Testing - -```swift -@Test("fetches profile asynchronously") -func fetchProfileAsync() async throws { - let service = ProfileService(client: MockHTTPClient()) - - let profile = try await service.fetchProfile(userID: "1") - - #expect(profile.name == "Alice") -} -``` - -## UI Testing - -UI tests use `XCUIApplication` to interact with the app as a user would. Always prefer accessibility identifiers over text matching for resilient tests. - -```swift -final class LoginUITests: XCTestCase { - let app = XCUIApplication() - - override func setUp() { - super.setUp() - continueAfterFailure = false - app.launchArguments = ["--uitesting"] - app.launch() - } - - func testSuccessfulLogin() { - let emailField = app.textFields["login.emailField"] - let passwordField = app.secureTextFields["login.passwordField"] - let loginButton = app.buttons["login.submitButton"] - - emailField.tap() - emailField.typeText("alice@example.com") - - passwordField.tap() - passwordField.typeText("password123") - - loginButton.tap() - - let welcomeLabel = app.staticTexts["home.welcomeLabel"] - XCTAssertTrue(welcomeLabel.waitForExistence(timeout: 5)) - XCTAssertEqual(welcomeLabel.label, "Welcome, Alice") - } -} -``` - -Set accessibility identifiers in production code: - -```swift -emailTextField.accessibilityIdentifier = "login.emailField" -passwordTextField.accessibilityIdentifier = "login.passwordField" -submitButton.accessibilityIdentifier = "login.submitButton" -``` - -## Test Doubles: Protocol-Based Mocking - -Swift's protocol-oriented design makes mocking straightforward. Define dependencies as protocols, then provide mock implementations in tests. - -```swift -// Production protocol -protocol HTTPClient { - func data(from url: URL) async throws -> (Data, URLResponse) -} - -// Production implementation -struct URLSessionHTTPClient: HTTPClient { - let session: URLSession - - func data(from url: URL) async throws -> (Data, URLResponse) { - try await session.data(from: url) - } -} - -// Test double -final class MockHTTPClient: HTTPClient { - var stubbedData: Data = Data() - var stubbedResponse: URLResponse = HTTPURLResponse() - var capturedURLs: [URL] = [] - - func data(from url: URL) async throws -> (Data, URLResponse) { - capturedURLs.append(url) - return (stubbedData, stubbedResponse) - } -} -``` - -### Dependency Injection - -Inject dependencies through initializers to make classes testable: - -```swift -final class ProfileService { - private let client: HTTPClient - - init(client: HTTPClient) { - self.client = client - } - - func fetchProfile(userID: String) async throws -> Profile { - let url = URL(string: "https://api.example.com/users/\(userID)")! - let (data, _) = try await client.data(from: url) - return try JSONDecoder().decode(Profile.self, from: data) - } -} -``` - -## Key Conventions - -- **One assertion per concept** -- a single test can have multiple assertions if they verify the same logical behavior, but avoid testing unrelated things together. -- **Arrange-Act-Assert** -- structure every test into setup, execution, and verification phases. -- **Name tests descriptively** -- `testFetchUser_withExpiredToken_throwsAuthError` is better than `testFetch2`. -- **Prefer Swift Testing for new code** -- use `@Test` and `#expect` when targeting Swift 5.9+; fall back to XCTest for older targets or UI tests. -- **Ensure test independence** -- each test must be runnable in isolation; always produce self-contained test state. diff --git a/skills/infrastructure/fish-shell-config/SKILL.md b/skills/infrastructure/fish-shell-config/SKILL.md deleted file mode 100644 index 17525896e..000000000 --- a/skills/infrastructure/fish-shell-config/SKILL.md +++ /dev/null @@ -1,299 +0,0 @@ ---- -name: fish-shell-config -promoted_to: shell-config -description: "Fish shell configuration and PATH management." -user-invocable: false -allowed-tools: - - Read - - Write - - Bash - - Grep - - Glob - - Edit -routing: - category: process - not_for: "fishing for bugs (debugging), fishing for feedback (asking for input), 'fish out' as 'extract/find', fish (the animal) — only fires for the Fish shell config" - triggers: - - fish - - fish shell - - config.fish - - abbr - - fish function - - fish_config - - fish_variables - - funced - - funcsave - - ~/.config/fish - - conf.d - - fish abbreviation - - .fish file - - "#!/usr/bin/env fish" - pairs_with: [] - force_route: true ---- - -# Fish Shell Configuration Skill - -Fish is not POSIX. Every pattern here targets Fish 3.0+ (supports `$()`, `&&`, `||`). Fish 4.0 (Rust rewrite) has no syntax changes. All generated code must use Fish-native syntax exclusively — never emit Bash constructs (`VAR=value`, `[[ ]]`, `export`, heredocs) in Fish contexts. - -## Reference Loading Table - -| Signal | Load These Files | Why | -|---|---|---| -| migrations | `bash-migration.md` | Loads detailed guidance from `bash-migration.md`. | -| editing fish config: variable assignment, PATH management, conditionals, tool integration | `fish-preferred-patterns.md` | Loads detailed guidance from `fish-preferred-patterns.md`. | -| syntax lookup: variable scope, PATH, functions, abbreviations, conditionals, debugging | `fish-quick-reference.md` | Loads detailed guidance from `fish-quick-reference.md`. | -| wiring dev tools into fish: Go, Rust, Node, Python, Docker, PATH and init hooks | `tool-integrations.md` | Loads detailed guidance from `tool-integrations.md`. | - -## Instructions - -### Step 1: Confirm Fish Context - -Before writing any shell code, confirm the target is Fish: - -- `$SHELL` contains `fish`, or -- Target file has `.fish` extension, or -- Target directory is `~/.config/fish/` - -If none of these hold, stop — this skill does not apply to Bash, Zsh, or POSIX shells. - -### Step 2: Choose the Correct File Location - -Place configuration in `conf.d/` modules with numeric prefixes for ordering — keep `config.fish` minimal. A monolithic `config.fish` with hundreds of lines is slow to load, hard to maintain, and impossible to selectively disable. - -**Directory layout**: -``` -~/.config/fish/ -├── config.fish # Minimal — interactive-only init -├── fish_variables # Auto-managed by Fish (never edit) -├── conf.d/ # Auto-sourced in alphabetical order -│ ├── 00-path.fish -│ ├── 10-env.fish -│ └── 20-abbreviations.fish -├── functions/ # Autoloaded functions (one per file) -│ ├── fish_prompt.fish -│ └── mkcd.fish -└── completions/ # Custom completions - └── mycommand.fish -``` - -**Decision tree**: -| What you're writing | Where it goes | -|---------------------|---------------| -| PATH additions | `conf.d/00-path.fish` | -| Environment variables | `conf.d/10-env.fish` | -| Abbreviations | `conf.d/20-abbreviations.fish` | -| Tool integrations | `conf.d/30-tools.fish` | -| Named function | `functions/.fish` | -| Custom prompt | `functions/fish_prompt.fish` | -| Completions | `completions/.fish` | -| One-time interactive init | `config.fish` (inside `status is-interactive`) | - -### Step 3: Write Variables - -Variable assignment is always `set VAR value` — never `VAR=value` (syntax error in Fish) or `export VAR=value`. - -```fish -set -l VAR value # Local — current block only -set -f VAR value # Function — entire function scope -set -g VAR value # Global — current session -set -U VAR value # Universal — persists across sessions (use sparingly) -set -x VAR value # Export — visible to child processes -set -gx VAR value # Global + Export (typical for env vars) -set -e VAR # Erase variable -set -q VAR # Test if set (silent, for conditionals) -``` - -Every Fish variable is a list. Never use colon-separated strings for PATH or similar variables — `set PATH "$PATH:/new/path"` creates a single malformed element because Fish PATH is a list, not a colon-delimited string. - -### Step 4: Manage PATH - -Use `fish_add_path` for PATH manipulation — it handles deduplication and persistence automatically. Manual `set PATH` only for session-scoped overrides. - -```fish -# CORRECT: fish_add_path handles deduplication and persistence -fish_add_path ~/.local/bin -fish_add_path ~/.cargo/bin -fish_add_path -P ~/go/bin # -P = session only, no persist - -# CORRECT: Direct manipulation when needed (session only) -set -gx PATH ~/custom/bin $PATH - -# WRONG: Colon-separated string — Fish PATH is a list -# set PATH "$PATH:/new/path" -``` - -### Step 5: Write Functions - -The autoloaded function filename must match the function name exactly — `functions/foo.fish` must contain `function foo`. A mismatch causes "Unknown command" errors. - -```fish -# ~/.config/fish/functions/mkcd.fish -function mkcd --description "Create directory and cd into it" - mkdir -p $argv[1] - and cd $argv[1] -end -``` - -Functions with argument parsing: -```fish -function backup --description "Create timestamped backup" - argparse 'd/dest=' 'h/help' -- $argv - or return - - if set -q _flag_help - echo "Usage: backup [-d destination] file..." - return 0 - end - - set -l dest (set -q _flag_dest; and echo $_flag_dest; or echo ".") - for file in $argv - set -l ts (date +%Y%m%d_%H%M%S) - cp $file $dest/(basename $file).$ts.bak - end -end -``` - -### Step 6: Choose Between Abbreviations, Functions, and Aliases - -| Use Case | Mechanism | Why | -|----------|-----------|-----| -| Simple shortcut | `abbr -a g git` | Expands in-place, visible in history | -| Needs arguments/logic | `function` in `functions/` | Full programming, works in scripts | -| Wrapping a command | `alias ll "ls -la"` | Convenience; creates function internally | - -Abbreviations are interactive-only — they do not work in scripts. Always wrap them in an interactive guard because they have no effect during non-interactive sourcing: - -```fish -# Always guard abbreviations -if status is-interactive - abbr -a g git - abbr -a ga "git add" - abbr -a gc "git commit" - abbr -a gst "git status" - abbr -a dc "docker compose" -end -``` - -### Step 7: Write Conditionals and Control Flow - -Use the `test` builtin for conditionals — never `[[ ]]` (syntax error in Fish) or `[ ]` (calls external `/bin/[`, slower than the builtin). Fish has no word splitting, so `$var` and `"$var"` behave identically — quote only when you need to prevent list expansion or preserve empty strings. - -```fish -# Conditionals — use 'test', not [[ ]] -if test -f config.json - echo "exists" -else if test -d config - echo "is directory" -end - -# Command chaining (both styles work in Fish 3.0+) -mkdir build && cd build && cmake .. -mkdir build; and cd build; and cmake .. - -# Loops -for file in *.fish - echo "Processing $file" -end - -# Switch -switch $argv[1] - case start - echo "Starting..." - case stop - echo "Stopping..." - case "*" - echo "Unknown: $argv[1]" - return 1 -end -``` - -### Step 8: Integrate External Tools - -Guard every tool integration with `type -q` so the config works on machines where the tool is not installed: - -```fish -# ~/.config/fish/conf.d/30-tools.fish -if type -q starship - starship init fish | source -end - -if type -q direnv - direnv hook fish | source -end - -if type -q fzf - fzf --fish | source -end - -# Homebrew (macOS) -if test -x /opt/homebrew/bin/brew - eval (/opt/homebrew/bin/brew shellenv) -end - -# Nix -if test -e /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.fish - source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.fish -end -``` - -### Step 9: Verify - -1. **Syntax check** — run `fish -n ` (parse without executing) -2. **Function name match** — verify filename matches function name for every file in `functions/` -3. **Interactive guards** — verify `status is-interactive` guards on abbreviations and key bindings in `conf.d/` -4. **Clean environment test** — run `fish --no-config` then `source ` to confirm isolated correctness - ---- - -## Reference Material - -### Example: Setting Up a New Fish Config -User says: "Set up my Fish shell config" -1. Confirm Fish context -2. Create modular structure in `~/.config/fish/` -3. Write `conf.d/00-path.fish`, `conf.d/10-env.fish`, `conf.d/20-abbreviations.fish` -4. Syntax-check all files - -### Example: Migrating a Bash Alias File -User says: "Convert my .bash_aliases to Fish" -1. Read `.bash_aliases`, confirm Fish target -2. Determine which become abbreviations vs functions -3. Write abbreviations to `conf.d/`, functions to `functions/` -4. Syntax-check, test in clean shell - ---- - -## Error Handling - -### Error: "Unknown command" for new function -Cause: Filename does not match function name -Solution: Ensure `functions/foo.fish` contains exactly `function foo`. Check for typos in both the filename and the function declaration. - -### Error: PATH changes not persisting across sessions -Cause: Used `set -gx PATH` (session-only) instead of `fish_add_path` (writes to universal `fish_user_paths`) -Solution: Use `fish_add_path /new/path` which persists by default, or use `set -U fish_user_paths /path $fish_user_paths` explicitly. - -### Error: Abbreviations not expanding in scripts -Cause: Abbreviations are interactive-only by design -Solution: Use a function instead. Move the logic from `abbr` to a file in `functions/`. - -### Error: Variable not visible to child process -Cause: Missing `-x` (export) flag on `set` -Solution: Use `set -gx VAR value` to make variable visible to subprocesses. Check with `set --show VAR` to inspect current scope and export status. - ---- - -## References - -| Task Signal | Load | Why | -|-------------|------|-----| -| Migrating from Bash, converting `.bashrc`/`.bash_aliases`, `source`, `export`, `[[` in Fish file | `bash-migration.md` | Full Bash-to-Fish syntax translation table | -| Variable scoping, PATH management, `fish_add_path`, `set` flags, `abbr`, completions | `fish-quick-reference.md` | Variable scope guide, special variables, control flow cheatsheet | -| Error audit, "unknown command", PATH not persisting, abbreviation not working, syntax error, broken conf.d | `fish-preferred-patterns.md` | Failure modes with grep detection commands and error-fix mappings | -| Go, Rust, Docker, Node.js, Python, pyenv, fnm, starship, direnv, fzf, zoxide, mise, tool setup | `tool-integrations.md` | Concrete integration patterns for common dev tools | - -- `${CLAUDE_SKILL_DIR}/references/bash-migration.md`: Complete Bash-to-Fish syntax translation table -- `${CLAUDE_SKILL_DIR}/references/fish-quick-reference.md`: Variable scoping, special variables, and command cheatsheet -- `${CLAUDE_SKILL_DIR}/references/fish-preferred-patterns.md`: Failure mode catalog with grep detection commands and error-fix mappings -- `${CLAUDE_SKILL_DIR}/references/tool-integrations.md`: Concrete integration patterns for Go, Rust, Docker, Node.js, Python, and shell enhancers diff --git a/skills/infrastructure/fish-shell-config/references/bash-migration.md b/skills/infrastructure/fish-shell-config/references/bash-migration.md deleted file mode 100644 index a3122b995..000000000 --- a/skills/infrastructure/fish-shell-config/references/bash-migration.md +++ /dev/null @@ -1,149 +0,0 @@ -# Bash to Fish Migration Reference - -## Syntax Translation Table - -| Bash | Fish | Notes | -|------|------|-------| -| `VAR=value` | `set VAR value` | Always use `set` | -| `export VAR=value` | `set -gx VAR value` | Global + Export | -| `unset VAR` | `set -e VAR` | Erase variable | -| `$?` | `$status` | Exit status of last command | -| `$@` or `$*` | `$argv` | Function/script arguments | -| `$#` | `(count $argv)` | Argument count | -| `${var:-default}` | `set -q var; or set var default` | Default value pattern | -| `$(command)` | `$(command)` or `(command)` | Both work in Fish 3.0+ | -| `[[ condition ]]` | `test condition` | Fish builtin | -| `[ condition ]` | `test condition` | Prefer Fish builtin | -| `&&` | `&&` or `; and` | Both work in Fish 3.0+ | -| `\|\|` | `\|\|` or `; or` | Both work in Fish 3.0+ | -| `function name() { }` | `function name ... end` | No braces in Fish | -| `source file` | `source file` | Same syntax | -| `heredoc < file.txt - -# Echo with embedded newlines -echo "line 1 -line 2 -line 3" > file.txt -``` - -### Process Substitution -```bash -# Bash -diff <(sort file1) <(sort file2) -``` -```fish -# Fish — use psub -diff (sort file1 | psub) (sort file2 | psub) -``` - -### Inline Environment for Commands -```bash -# Bash -LANG=C sort file.txt -``` -```fish -# Fish -env LANG=C sort file.txt -``` - -## Key Differences to Remember - -1. **No word splitting**: `$var` and `"$var"` behave identically in Fish. Quotes are not needed to prevent word splitting. - -2. **1-indexed arrays**: Fish arrays start at 1, not 0. `$array[1]` is the first element. - -3. **Variables are always lists**: Even a single-element variable is a one-element list. This is fundamental to Fish. - -4. **No inline assignment**: `VAR=value command` does not work. Use `env VAR=value command` or `set -lx VAR value; command`. - -5. **`test` not brackets**: Use `test -f file` not `[[ -f file ]]`. The `test` builtin is fast and native. - -6. **`end` not braces/fi/done**: All blocks close with `end` — `if/end`, `for/end`, `while/end`, `function/end`, `switch/end`. - -7. **No `$()` needed for arithmetic**: Fish has `math` builtin: `math "2 + 2"` returns `4`. diff --git a/skills/infrastructure/fish-shell-config/references/fish-preferred-patterns.md b/skills/infrastructure/fish-shell-config/references/fish-preferred-patterns.md deleted file mode 100644 index ebf33a8bd..000000000 --- a/skills/infrastructure/fish-shell-config/references/fish-preferred-patterns.md +++ /dev/null @@ -1,240 +0,0 @@ -# Fish Shell Configuration Patterns - -> **Scope**: Fish-native patterns for variable assignment, PATH management, conditionals, and tool integration. Covers the correct way to configure Fish 3.0+ (including Fish 4.0 Rust rewrite, which has identical syntax). -> **Version range**: Fish 3.0+ -> **Generated**: 2026-04-17 - ---- - -## Overview - -Fish configuration errors rarely throw loud errors — they silently produce wrong behavior: PATH entries that vanish after restart, abbreviations that expand in the terminal but break scripts, environment variables invisible to child processes. The patterns below show the Fish-native approach for each common task, with detection commands to find violations in existing configs. - ---- - -## Pattern Catalog - -### Use Fish-Native Variable Assignment - -Assign variables with `set` and the appropriate scope flag. Use `-gx` for environment variables (global + exported) and `-g` for shell-internal variables. - -```fish -set -gx GOPATH ~/go -set -g MY_VAR hello -``` - -**Why this matters**: Fish has never supported `VAR=value` syntax. That form is a parse error that causes the entire conf.d file to fail to load — all subsequent lines in that file are skipped. - -**Detection**: -```bash -grep -rn '^[A-Z_a-z]*=[^=]' --include="*.fish" ~/.config/fish/ -``` - ---- - -### Use `set -gx` Instead of `export` - -Set environment variables with `set -gx VAR value`. For PATH additions, use `fish_add_path`. - -```fish -fish_add_path ~/.local/bin -set -gx EDITOR nvim -``` - -**Why this matters**: `export` is not a Fish builtin for variable assignment. The syntax `export VAR=value` is a parse error. Even `export EDITOR nvim` (space-separated) calls the external `export` binary, sets nothing in Fish scope, and is immediately discarded. - -**Detection**: -```bash -grep -rn 'export ' --include="*.fish" ~/.config/fish/ -``` - ---- - -### Use List Syntax for PATH Management - -Add PATH entries with `fish_add_path` (preferred) or list-style `set`. Never concatenate with colons — Fish PATH is a list, not a colon-separated string. - -```fish -fish_add_path ~/.local/bin # Prepend, deduplicates, persists -fish_add_path --append ~/bin # Append to end -set -gx PATH ~/.local/bin $PATH # Session-only prepend (list syntax, not string) -``` - -**Why this matters**: String-concatenating with colons creates a single element containing the literal colon — e.g., `"/home/user/.local/bin:/usr/bin"` becomes one list entry, not two PATH components. Commands in the new directory become unfindable. - -**Detection**: -```bash -grep -rn 'PATH.*:.*PATH\|"\$PATH:' --include="*.fish" ~/.config/fish/ -``` - ---- - -### Guard Abbreviations With `status is-interactive` - -Wrap `abbr` calls in an interactive check. Abbreviations are interactive-only — they never expand in scripts, CI, or `fish -c "..."` invocations. - -```fish -# ~/.config/fish/conf.d/20-abbreviations.fish -if status is-interactive - abbr -a g git - abbr -a gst "git status" - abbr -a gc "git commit" -end -``` - -**Why this matters**: When Fish sources conf.d/ in non-interactive mode (scripts, CI, cron), `abbr -a` emits a warning and is a no-op. Logic depending on abbreviation expansion in .fish scripts will silently fail. - -**Detection**: -```bash -grep -rn '^abbr ' --include="*.fish" ~/.config/fish/conf.d/ -``` - ---- - -### Use the `test` Builtin for Conditionals - -Write conditionals with `test` or its equivalent forms. Fish does not support `[[` (Bash/Zsh construct) and `[ ]` calls an external binary with different behavior. - -```fish -if test -f ~/.config/fish/local.fish - source ~/.config/fish/local.fish -end - -if test -z "$TMUX" - echo "not in tmux" -end -``` - -**Why this matters**: `[[` is not recognized by Fish and causes a parse error. `[ ]` calls the external `/bin/[` binary which has slightly different behavior from the Fish `test` builtin and incurs a fork/exec overhead. - -**Detection**: -```bash -grep -rn '\[\[' --include="*.fish" ~/.config/fish/ -grep -rn 'if \[' --include="*.fish" ~/.config/fish/ -``` - ---- - -### Match Function Names to Filenames - -When defining autoloaded functions, the function name inside the file must exactly match the filename (without `.fish` extension). Fish autoloads functions by filename lookup. - -```fish -# ~/.config/fish/functions/mkcd.fish — function name must match filename -function mkcd --description "Create directory and cd into it" - mkdir -p $argv[1] - and cd $argv[1] -end -``` - -**Why this matters**: When `mkcd` is called, Fish looks for `functions/mkcd.fish`. If that file defines `function make_directory_and_cd` instead, calling `mkcd` fails with "Unknown command: mkcd". The file loads but the desired name never registers. - -**Detection**: -```bash -for f in ~/.config/fish/functions/*.fish; do - name=$(basename "$f" .fish) - grep -qL "function $name" "$f" && echo "MISMATCH: $f" -done -``` - ---- - -### Guard Tool Integrations With `type -q` - -Wrap every external tool initialization in a `type -q` check. This prevents "command not found" errors on machines where the tool is not installed. - -```fish -if type -q starship - starship init fish | source -end - -if type -q direnv - direnv hook fish | source -end - -if type -q fnm - fnm env --use-on-cd | source -end -``` - -**Why this matters**: If the tool is not installed, the bare init command fails with "command not found: starship". This error may stop further conf.d/ processing and produces error output for every new shell opened on machines without that tool. - -**Detection**: -```bash -grep -rn 'init fish | source\|hook fish | source' --include="*.fish" ~/.config/fish/conf.d/ -rg '(starship|direnv|fnm|zoxide|mise|pyenv) (init|hook)' --type-add 'fish:*.fish' --type fish ~/.config/fish/conf.d/ -``` - ---- - -### Use `-gx` Instead of `-U` for Environment Variables in conf.d/ - -Set environment variables in conf.d/ files with `set -gx` (global exported), not `set -U` (universal). Universal variables are for interactive user preferences set from the command line, not for deployed configuration. - -```fish -# Use -gx for environment variables in conf.d/ files -set -gx GOPATH ~/go -set -gx EDITOR nvim -set -gx JAVA_HOME /usr/lib/jvm/java-17-openjdk -``` - -**Why this matters**: Universal variables persist in `fish_variables` across all sessions. Setting them in conf.d/ re-sets them on every shell start — harmless for idempotent values but causes subtle bugs when the value changes (e.g., different JAVA_HOME on different machines sharing dotfiles). - -**Detection**: -```bash -grep -rn 'set -U\|set --universal' --include="*.fish" ~/.config/fish/conf.d/ -``` - ---- - -## Error-Fix Mappings - -| Error / Symptom | Root Cause | Fix | -|-----------------|------------|-----| -| `Unknown command: myfunction` | `functions/myfunction.fish` defines wrong function name | Rename function inside file to match filename exactly | -| PATH entry disappears after reboot | Used `set -gx PATH ...` (session-only) | Replace with `fish_add_path /path` | -| Abbreviation works in terminal, not in script | Abbreviations are interactive-only by design | Use a function in `functions/` instead | -| Parse error on shell start | `export VAR=value` or `VAR=value` in `.fish` file | Replace with `set -gx VAR value` | -| Garbled PATH with colons visible | `set PATH "$PATH:/new"` colon concatenation | Replace with `fish_add_path /new` | -| `[[: command not found` | Bash `[[` brackets used in Fish file | Replace with `test` builtin | -| Tool init error on every new shell | Init call without `type -q` guard | Wrap in `if type -q toolname; ... end` | -| Universal variable duplicated in PATH | `fish_user_paths` has duplicate entries | `set -U fish_user_paths (string split \n (printf "%s\n" $fish_user_paths \| sort -u))` | -| conf.d changes not taking effect | File has syntax error from Bash constructs | Run `fish -n ~/.config/fish/conf.d/myfile.fish` to check | - ---- - -## Detection Commands Reference - -```bash -# Bash-style assignment -grep -rn '^[A-Z_a-z]*=[^=]' --include="*.fish" ~/.config/fish/ - -# Bash export syntax -grep -rn 'export ' --include="*.fish" ~/.config/fish/ - -# Colon PATH manipulation -grep -rn '"\$PATH:' --include="*.fish" ~/.config/fish/ - -# Unguarded abbreviations -grep -rn '^abbr ' --include="*.fish" ~/.config/fish/conf.d/ - -# Bash bracket conditionals -grep -rn '\[\[' --include="*.fish" ~/.config/fish/ - -# Tool integrations without availability guard -grep -rn 'init fish | source\|hook fish | source' --include="*.fish" ~/.config/fish/conf.d/ - -# Universal variables set in conf.d (likely should be -gx) -grep -rn 'set -U ' --include="*.fish" ~/.config/fish/conf.d/ - -# Syntax check all conf.d files -for f in ~/.config/fish/conf.d/*.fish; do fish -n "$f" || echo "SYNTAX ERROR: $f"; done -``` - ---- - -## See Also - -- `bash-migration.md` — Bash-to-Fish syntax translation table -- `fish-quick-reference.md` — Variable scoping, PATH management, control flow cheatsheet -- `tool-integrations.md` — Complete tool integration patterns (Go, Rust, Docker, Node.js, pyenv) diff --git a/skills/infrastructure/fish-shell-config/references/fish-quick-reference.md b/skills/infrastructure/fish-shell-config/references/fish-quick-reference.md deleted file mode 100644 index 009935843..000000000 --- a/skills/infrastructure/fish-shell-config/references/fish-quick-reference.md +++ /dev/null @@ -1,229 +0,0 @@ -# Fish Shell Quick Reference - -## Variable Scoping - -### Scope Flags -```fish -set -l VAR value # Local — current block only -set -f VAR value # Function — visible in entire function (not just current block) -set -g VAR value # Global — current session -set -U VAR value # Universal — persists across sessions (stored in fish_variables) -set -x VAR value # Export — visible to child processes -``` - -### Common Combinations -```fish -set -gx VAR value # Global + Export (typical for env vars in conf.d/) -set -Ux VAR value # Universal + Export (persist + export) -set -lx VAR value # Local + Export (temporary env for child process) -``` - -### Scope Guidelines - -| Scope | Flag | Use For | -|-------|------|---------| -| Local | `-l` | Loop variables, temporary values in functions | -| Function | `-f` | Variables shared across blocks within one function | -| Global | `-g` | Session-specific settings that shouldn't persist | -| Universal | `-U` | User preferences that should survive restarts | -| Export | `-x` | Environment variables needed by child processes | - -### Scope Examples -```fish -# Local: loop and temp variables -for file in *.txt - set -l basename (basename $file .txt) - echo $basename -end - -# Global: session settings -set -g current_project "my-app" - -# Universal: permanent preferences -set -U fish_greeting "" # Disable greeting permanently - -# Global + Export: env vars for tools -set -gx GOPATH ~/go -set -gx EDITOR nvim -``` - -## Special Variables - -```fish -$argv # Function/script arguments (list) -$status # Exit status of last command -$pipestatus # Exit statuses of all pipe components (list) -$fish_pid # PID of current Fish process -$USER # Current user -$HOME # Home directory -$PWD # Current working directory -$_ # Last argument of previous command -$SHLVL # Shell nesting level -$CMD_DURATION # Duration of last command in milliseconds -``` - -## PATH Management - -```fish -# Recommended: fish_add_path (handles deduplication) -fish_add_path ~/.local/bin # Prepend, persists via fish_user_paths -fish_add_path --prepend ~/.cargo/bin # Explicit prepend -fish_add_path --append ~/custom/bin # Append instead -fish_add_path --move ~/go/bin # Move existing entry to front -fish_add_path -P ~/.local/bin # Session only (modifies PATH, not fish_user_paths) - -# Manual: direct list manipulation (session only) -set -gx PATH ~/custom/bin $PATH # Prepend -set -gx PATH $PATH ~/custom/bin # Append - -# WRONG: colon-separated (Fish PATH is a list) -# set PATH "$PATH:/new/path" # Creates single malformed element -``` - -## Functions - -```fish -# Define (or place in ~/.config/fish/functions/name.fish) -function name --description "what it does" - # body -end - -# Interactive editing -funced name # Edit function in $EDITOR -funcsave name # Save function to ~/.config/fish/functions/name.fish - -# Inspection -functions # List all functions -functions name # Show function definition -type -q name # Check if function/command exists (silent) -``` - -## Abbreviations - -```fish -# Add (interactive only — always guard with status is-interactive) -abbr -a short "long command" - -# Remove -abbr -e short - -# List all -abbr - -# Example setup in conf.d/ -if status is-interactive - abbr -a g git - abbr -a ga "git add" - abbr -a gc "git commit" - abbr -a gp "git push" - abbr -a gst "git status" -end -``` - -## Testing and Conditionals - -```fish -# File tests -test -f file # File exists and is regular file -test -d dir # Directory exists -test -e path # Path exists (any type) -test -r file # Readable -test -w file # Writable -test -x file # Executable -test -s file # Exists and non-empty - -# String tests -test -z "$var" # String is empty -test -n "$var" # String is not empty -test "$a" = "$b" # String equality (single =) -test "$a" != "$b" # String inequality - -# Numeric tests -test $n -eq 5 # Equal -test $n -ne 5 # Not equal -test $n -gt 5 # Greater than -test $n -ge 5 # Greater or equal -test $n -lt 5 # Less than -test $n -le 5 # Less or equal - -# Combining -test -f file -a -r file # AND -test -f file -o -d file # OR -not test -f file # NOT -``` - -## Control Flow - -```fish -# If/else -if test -f config.json - echo "exists" -else if test -d config - echo "is directory" -else - echo "not found" -end - -# For loop -for file in *.fish - echo "Processing $file" -end - -# While loop -while read -l line - echo "Line: $line" -end < file.txt - -# Numeric loop -for i in (seq 1 10) - echo "Number: $i" -end - -# Switch -switch $argv[1] - case start - echo "Starting..." - case stop - echo "Stopping..." - case "*" - echo "Unknown: $argv[1]" - return 1 -end - -# Command chaining (both styles, Fish 3.0+) -mkdir build && cd build && cmake .. -mkdir build; and cd build; and cmake .. -test -d build || mkdir build -test -d build; or mkdir build -``` - -## Debugging and Troubleshooting - -```fish -# Syntax check without executing -fish -n script.fish - -# Debug categories -fish --print-debug-categories -fish --debug='*config*' --debug-output=/tmp/fish-debug.log - -# Clean environment test -fish --no-config - -# Inspect variable scope and value -set --show VAR - -# Check if interactive or login -status is-interactive; and echo "interactive" -status is-login; and echo "login" -``` - -## Common Troubleshooting - -| Problem | Cause | Solution | -|---------|-------|---------| -| "Unknown command" for new function | Filename/function name mismatch | `functions/foo.fish` must contain `function foo` | -| PATH changes not persisting | Used `set -gx PATH` | Use `fish_add_path` (writes to `fish_user_paths`) | -| Abbreviations not working in scripts | Interactive-only by design | Use functions instead | -| Env var not visible to subprocess | Missing `-x` flag | Use `set -gx VAR value` | -| Universal variable stale | `fish_variables` file conflict | `set -e VAR` then `set -U VAR value` | diff --git a/skills/infrastructure/fish-shell-config/references/tool-integrations.md b/skills/infrastructure/fish-shell-config/references/tool-integrations.md deleted file mode 100644 index 2bacd51c5..000000000 --- a/skills/infrastructure/fish-shell-config/references/tool-integrations.md +++ /dev/null @@ -1,318 +0,0 @@ -# Fish Shell Tool Integrations - -> **Scope**: Concrete fish shell integration patterns for common dev tools (Go, Rust, Docker, Node.js, Python, and shell enhancers). Covers PATH setup, init hooks, and abbreviation patterns. Does not cover tool installation. -> **Version range**: Fish 3.0+; tool-version notes inline where behavior differs -> **Generated**: 2026-04-17 - ---- - -## Overview - -Each tool integration follows the same three-part structure: (1) PATH and env setup in `conf.d/00-path.fish` or `conf.d/10-env.fish`, (2) init hook with `type -q` guard in `conf.d/30-tools.fish`, (3) abbreviations in `conf.d/20-abbreviations.fish` guarded by `status is-interactive`. Copy-paste sections into the appropriate file. - ---- - -## Pattern Table - -| Tool | Init Pattern | PATH Location | Version Notes | -|------|-------------|---------------|---------------| -| Go | `set -gx GOPATH` | `fish_add_path ~/go/bin` | Go 1.16+: `GOPATH` auto-set if unset | -| Rust/Cargo | none (cargo sets PATH via rustup) | `fish_add_path ~/.cargo/bin` | rustup 1.24+ writes to `fish_user_paths` on install | -| Node/fnm | `fnm env --use-on-cd \| source` | handled by fnm | fnm 1.32+ has native Fish support | -| Python/pyenv | `pyenv init - fish \| source` | `fish_add_path ~/.pyenv/bin` | pyenv 2.0+ Fish init command | -| Docker | none (PATH handled by install) | none | abbreviations only | -| starship | `starship init fish \| source` | none | requires starship 0.46+ for Fish | -| direnv | `direnv hook fish \| source` | none | requires direnv 2.21+ for Fish | -| fzf | `fzf --fish \| source` | none | `--fish` flag: fzf 0.48+; older: use fzf.fish plugin | -| zoxide | `zoxide init fish \| source` | none | all versions | -| mise/rtx | `mise activate fish \| source` | none | replaces asdf for Fish | - ---- - -## Go - -```fish -# conf.d/10-env.fish -set -gx GOPATH ~/go -set -gx GOROOT /usr/local/go # Only if Go installed outside $PATH -set -gx CGO_ENABLED 0 # Optional: disable CGO for pure Go builds - -# conf.d/00-path.fish -fish_add_path ~/go/bin # Your compiled binaries (go install output) -fish_add_path /usr/local/go/bin # Go toolchain itself (if not in /usr/bin) -``` - -```fish -# conf.d/20-abbreviations.fish — inside is-interactive guard -abbr -a gob "go build ./..." -abbr -a got "go test ./..." -abbr -a gotr "go test -race ./..." -abbr -a gom "go mod tidy" -abbr -a gor "go run ." -``` - -**Version note**: Go 1.16+ sets `GOPATH` to `~/go` automatically if unset. Explicitly setting it is harmless but redundant. `GOROOT` is only needed for non-standard Go installations. - ---- - -## Rust / Cargo - -```fish -# conf.d/00-path.fish -fish_add_path ~/.cargo/bin # All cargo-installed tools end up here -``` - -```fish -# conf.d/20-abbreviations.fish — inside is-interactive guard -abbr -a cb "cargo build" -abbr -a cbr "cargo build --release" -abbr -a ct "cargo test" -abbr -a cr "cargo run" -abbr -a cc "cargo check" -abbr -a ccl "cargo clippy" -abbr -a cf "cargo fmt" -``` - -**Note**: `rustup` writes a `~/.cargo/env` script for Bash. Ignore it — just add `~/.cargo/bin` to PATH as above. Rustup 1.24+ also tries to update `fish_user_paths` on install; `fish_add_path` is idempotent so running both is harmless. - ---- - -## Node.js — fnm (Recommended) - -```fish -# conf.d/30-tools.fish -if type -q fnm - fnm env --use-on-cd | source # Auto-switches Node version on cd -end -``` - -```fish -# conf.d/20-abbreviations.fish — inside is-interactive guard -if type -q fnm - abbr -a ni "npm install" - abbr -a nid "npm install --save-dev" - abbr -a nr "npm run" - abbr -a nx "npx" -end -``` - -**Version note**: `fnm env --use-on-cd` requires fnm 1.32+. For older fnm, use `fnm env | source` without `--use-on-cd` and manually run `fnm use` when changing projects. - -## Node.js — nvm (Alternative) - -```fish -# conf.d/30-tools.fish — nvm is Bash-native; use bass or nvm.fish wrapper -if test -d ~/.nvm - # Option A: nvm.fish plugin (preferred — native Fish) - # Install: fisher install jorgebucaran/nvm.fish - # No additional config needed after plugin install - - # Option B: bass (Bash source wrapper) - if type -q bass - function nvm - bass source ~/.nvm/nvm.sh --no-use ";" nvm $argv - end - end -end -``` - -**Note**: nvm is a Bash project. The `nvm.fish` Fisher plugin is a drop-in replacement that avoids the Bash interop complexity. Prefer fnm or nvm.fish over Bash-bridge patterns. - ---- - -## Python — pyenv - -```fish -# conf.d/00-path.fish -fish_add_path ~/.pyenv/bin -fish_add_path ~/.pyenv/shims - -# conf.d/30-tools.fish -if type -q pyenv - pyenv init - fish | source -end -``` - -```fish -# conf.d/20-abbreviations.fish — inside is-interactive guard -if type -q pyenv - abbr -a py "python" - abbr -a pip3 "python -m pip" -end -``` - -**Version note**: pyenv 2.0+ uses `pyenv init - fish`. Older versions (< 2.0) used `pyenv init -` without the `fish` argument — both work but the newer form is more explicit. - -## Python — virtualenv Activation - -```fish -# functions/venv.fish — activate/deactivate shortcut -function venv --description "Activate or create .venv" - if test -d .venv - source .venv/bin/activate.fish - else if test -n "$argv[1]" - python -m venv $argv[1] - source $argv[1]/bin/activate.fish - else - python -m venv .venv - source .venv/bin/activate.fish - end -end -``` - -**Why a function, not an abbreviation**: Activation requires `source`, which only works in Fish function context. - ---- - -## Docker / Docker Compose - -```fish -# conf.d/20-abbreviations.fish — inside is-interactive guard -if type -q docker - abbr -a d "docker" - abbr -a dc "docker compose" - abbr -a dcu "docker compose up" - abbr -a dcud "docker compose up -d" - abbr -a dcd "docker compose down" - abbr -a dcl "docker compose logs -f" - abbr -a dps "docker ps" - abbr -a dpsa "docker ps -a" - abbr -a drm "docker rm" - abbr -a drmi "docker rmi" - abbr -a dex "docker exec -it" -end -``` - -**Note**: `docker compose` (V2, no hyphen) is the current syntax. `docker-compose` (V1, with hyphen) is deprecated since Docker Desktop 4.20 / Docker Engine 23.0. - ---- - -## Shell Enhancers - -### starship (Cross-Shell Prompt) - -```fish -# conf.d/30-tools.fish -if type -q starship - starship init fish | source -end -``` - -**Version note**: starship 0.46+ has native Fish support. Requires Fish 3.0+. The `starship init fish` command outputs a Fish function definition — `source` evaluates it in the current shell. - -### direnv (Per-Directory Env) - -```fish -# conf.d/30-tools.fish -if type -q direnv - direnv hook fish | source -end -``` - -**Note**: direnv 2.21+ has native Fish hook support. The hook adds a `cd` event handler; it does not slow down non-cd commands. - -### fzf (Fuzzy Finder) - -```fish -# conf.d/30-tools.fish -if type -q fzf - fzf --fish | source # fzf 0.48+: native Fish key bindings - # For older fzf (< 0.48), use the fzf.fish Fisher plugin instead -end -``` - -**Version note**: `fzf --fish` (outputting Fish-native bindings) was introduced in fzf 0.48.0 (2024-01). For fzf < 0.48, the `fzf.fish` Fisher plugin provides equivalent Ctrl+R, Ctrl+T, Alt+C bindings. - -### zoxide (Smart cd Replacement) - -```fish -# conf.d/30-tools.fish -if type -q zoxide - zoxide init fish | source - # After init, 'z dirname' jumps to frecent match - # 'zi' opens interactive selection with fzf -end -``` - -### mise / rtx (Version Manager, asdf Replacement) - -```fish -# conf.d/30-tools.fish -if type -q mise - mise activate fish | source -end -``` - -**Note**: mise (formerly rtx) replaces asdf. The Fish activation hook manages shims automatically. Do not use `asdf` Fish integration alongside mise — they conflict. - ---- - -## Full conf.d Example: Complete Setup - -```fish -# ~/.config/fish/conf.d/30-tools.fish -# Tool integrations — all guarded with type -q - -# Prompt -if type -q starship - starship init fish | source -end - -# Directory env -if type -q direnv - direnv hook fish | source -end - -# Node version management -if type -q fnm - fnm env --use-on-cd | source -end - -# Python version management -if type -q pyenv - pyenv init - fish | source -end - -# Version manager (mise/rtx) -if type -q mise - mise activate fish | source -end - -# Fuzzy finder -if type -q fzf - fzf --fish | source -end - -# Smart directory navigation -if type -q zoxide - zoxide init fish | source -end - -# Homebrew (macOS only) -if test -x /opt/homebrew/bin/brew - eval (/opt/homebrew/bin/brew shellenv) -end -``` - ---- - -## Error-Fix Mappings - -| Error / Symptom | Root Cause | Fix | -|-----------------|------------|-----| -| `command not found: go` after install | Go bin not in PATH | `fish_add_path /usr/local/go/bin` | -| `cargo: command not found` | `~/.cargo/bin` not in PATH | `fish_add_path ~/.cargo/bin` | -| `fnm: Unknown version` on new shell | `--use-on-cd` not in init | Add `fnm env --use-on-cd \| source` | -| direnv not loading `.envrc` | direnv hook not sourced | Add `direnv hook fish \| source` to conf.d | -| `fzf --fish` flag unknown | fzf version < 0.48 | Install fzf.fish plugin via Fisher instead | -| `pyenv: command not found` | pyenv shims not in PATH | `fish_add_path ~/.pyenv/bin ~/.pyenv/shims` | -| nvm not found in Fish | nvm is Bash-only | Use nvm.fish plugin or fnm instead | -| `GOPATH` changes not persisting | Set with `set -g` instead of `set -gx` | Use `set -gx GOPATH ~/go` | - ---- - -## See Also - -- `fish-preferred-patterns.md` — Detection commands for common Fish config mistakes -- `bash-migration.md` — Bash-to-Fish syntax translation -- `fish-quick-reference.md` — Variable scoping and PATH management cheatsheet diff --git a/skills/infrastructure/kubernetes-debugging/SKILL.md b/skills/infrastructure/kubernetes-debugging/SKILL.md deleted file mode 100644 index 40850a196..000000000 --- a/skills/infrastructure/kubernetes-debugging/SKILL.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -name: kubernetes-debugging -promoted_to: kubernetes -description: "Kubernetes debugging for pod failures and networking." -user-invocable: false -context: fork -agent: kubernetes-helm-engineer -routing: - triggers: - - "kubernetes debug" - - "pod failure" - - "pod crashloop" - - "kubectl logs" - - "OOMKilled" - - "pod pending" - category: kubernetes - pairs_with: - - kubernetes-security - - service-health-check ---- - -# Kubernetes Debugging Skill - -Systematic diagnosis of pod failures, networking issues, and resource problems using a structured triage flow: describe, logs, events, exec. - -## Reference Loading Table - -| Signal | Reference | Size | -|--------|-----------|------| -| CrashLoopBackOff, OOMKilled, config error, health check, liveness probe, ImagePullBackOff, image pull, registry auth, Pending, FailedScheduling, node affinity, taint, PVC | `references/crash-diagnosis.md` | ~140 lines | -| service resolution, DNS, nslookup, CoreDNS, port-forward, NetworkPolicy, ingress, egress | `references/network-debugging.md` | ~50 lines | -| CPU throttling, memory limit, OOMKill, ephemeral storage, DiskPressure, debug container, distroless, kubectl reference, rollout, exec | `references/resource-debugging.md` | ~100 lines | - -**Loading rule.** Read the references whose signals match the task before responding. - -## Instructions - -### Triage Flow - -Follow this sequence for every pod or workload issue. Do not skip steps -- many failures (scheduling, image pull, volume mount) are only visible in events and describe output, not in logs, so jumping straight to logs misses them. - -Always specify `-n ` explicitly in every command; never rely on the default context namespace, because the wrong namespace silently returns empty or misleading results. - -```bash -# 1. Get an overview of the resource state -kubectl get pods -n -o wide - -# 2. Describe the resource for events, conditions, and status -kubectl describe pod -n - -# 3. Check current container logs -kubectl logs -n -c - -# 4. Check previous container logs (critical for CrashLoopBackOff) -# Always check --previous before current logs for crashed containers, -# because deleting or restarting the pod destroys these logs permanently. -kubectl logs -n -c --previous - -# 5. Check namespace events sorted by time -kubectl get events -n --sort-by='.lastTimestamp' - -# 6. If the container is running, exec in for live inspection -kubectl exec -it -n -c -- /bin/sh -``` - -Use read-only commands (describe, logs, get) to gather evidence before proposing any modifications. Never suggest changes based on assumptions -- gather diagnostic output first. - -### Diagnosis Routing - -Based on triage output, load the appropriate reference and follow its diagnosis flow: - -| Symptom | Reference | -|---------|-----------| -| Pod status CrashLoopBackOff, ImagePullBackOff, or Pending | `references/crash-diagnosis.md` | -| Service unreachable, DNS failure, connection refused | `references/network-debugging.md` | -| CPU throttling, OOMKill, disk pressure, need debug container | `references/resource-debugging.md` | - -### Error: "no endpoints available for service" -Cause: The Service selector does not match any running pod labels. -Solution: Compare `kubectl get svc -o yaml` selector with `kubectl get pods --show-labels`. Fix the label mismatch. - ---- - -## References - -- [kubernetes-security skill](../kubernetes-security/SKILL.md) -- NetworkPolicy patterns and RBAC debugging diff --git a/skills/infrastructure/kubernetes-debugging/references/crash-diagnosis.md b/skills/infrastructure/kubernetes-debugging/references/crash-diagnosis.md deleted file mode 100644 index 5b3f03e1d..000000000 --- a/skills/infrastructure/kubernetes-debugging/references/crash-diagnosis.md +++ /dev/null @@ -1,162 +0,0 @@ -# Crash Diagnosis Reference - -> **Scope**: CrashLoopBackOff (OOMKilled, config errors, health check failures), ImagePullBackOff (auth, tags, network), and Pending pod diagnosis (scheduling, affinity, taints, PVC). Does NOT cover networking or resource monitoring. -> **Version range**: Kubernetes 1.25+ (ephemeral containers assumed available) -> **Generated**: 2026-04-16 - ---- - -## CrashLoopBackOff Diagnosis - -CrashLoopBackOff means the container starts, exits, and Kubernetes restarts it with exponential backoff. Do not `kubectl delete pod` to "fix" this -- the replacement pod will crash the same way, and you lose the previous container's logs. Read `--previous` logs and describe events first. - -### OOMKilled -- container exceeded memory limit - -```bash -# Check termination reason -kubectl describe pod -n | grep -A 5 "Last State" -# Look for: Reason: OOMKilled - -# Check resource limits vs actual usage -kubectl top pod -n -``` - -Fix: Increase `resources.limits.memory` or fix the memory leak in the application. Do not blindly increase limits without checking actual usage first -- over-provisioning wastes cluster resources, and a memory leak will eventually exceed any limit you set. Run `kubectl top pod` under realistic load, then set limits to 1.5-2x observed peak. - -### Application configuration error -- missing env vars, bad config file, wrong DB host - -```bash -# Check logs from the previous (crashed) instance -kubectl logs -n --previous - -# Verify environment variables are set correctly -kubectl exec -n -- env | sort - -# Check if ConfigMap/Secret is mounted -kubectl describe pod -n | grep -A 10 "Mounts" -``` - -### Health check failure -- liveness probe kills the container - -```bash -# Check probe configuration -kubectl describe pod -n | grep -A 10 "Liveness" - -# Common issues: -# - initialDelaySeconds too short for slow-starting apps -# - Probe endpoint returns non-200 during startup -# - Probe timeout too aggressive -``` - -```yaml -# Fix: add a startup probe for slow-starting applications -spec: - containers: - - name: app - startupProbe: - httpGet: - path: /healthz - port: 8080 - failureThreshold: 30 - periodSeconds: 10 - livenessProbe: - httpGet: - path: /healthz - port: 8080 - periodSeconds: 10 - timeoutSeconds: 5 -``` - ---- - -## ImagePullBackOff Diagnosis - -The kubelet cannot pull the container image. - -```bash -# Check the exact error -kubectl describe pod -n | grep -A 5 "Events" -# Look for: Failed to pull image, ErrImagePull, ImagePullBackOff -``` - -### Registry authentication -- private registry requires credentials - -```bash -# Verify imagePullSecrets exist on the pod -kubectl get pod -n -o jsonpath='{.spec.imagePullSecrets}' - -# Check if the secret exists and has valid data -kubectl get secret -n -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d -``` - -### Image tag does not exist -- typo or tag was overwritten - -```bash -# Verify the image reference -kubectl get pod -n -o jsonpath='{.spec.containers[0].image}' - -# Test pulling manually on a node or locally -docker pull -``` - -Do not trust `:latest` tags -- they are mutable, so the image you tested locally may differ from what the node pulled. Always use image digests or immutable tags, and verify the exact image reference with the jsonpath command above. - -### Network issues -- node cannot reach the registry - -```bash -# Check if the node has network access to the registry -kubectl debug node/ -it --image=busybox -- nslookup registry.example.com -``` - ---- - -## Pending Pods - -Pods stuck in Pending state have not been scheduled to any node. - -```bash -# Check scheduler events -kubectl describe pod -n | grep -A 10 "Events" -# Look for: FailedScheduling, Insufficient cpu, Insufficient memory -``` - -### Resource constraints -- not enough CPU or memory on any node - -```bash -# Check node capacity and allocatable resources -kubectl describe nodes | grep -A 5 "Allocated resources" - -# Check resource requests across the cluster -kubectl top nodes -``` - -### Node affinity or node selector mismatch - -```bash -# Check what the pod requires -kubectl get pod -n -o jsonpath='{.spec.nodeSelector}' -kubectl get pod -n -o jsonpath='{.spec.affinity}' - -# Check node labels -kubectl get nodes --show-labels -``` - -### Taints and tolerations - -```bash -# Check node taints -kubectl describe nodes | grep -A 3 "Taints" - -# Check pod tolerations -kubectl get pod -n -o jsonpath='{.spec.tolerations}' -``` - -### PersistentVolumeClaim not bound - -```bash -# Check PVC status -kubectl get pvc -n - -# Describe the PVC for events -kubectl describe pvc -n -``` diff --git a/skills/infrastructure/kubernetes-debugging/references/network-debugging.md b/skills/infrastructure/kubernetes-debugging/references/network-debugging.md deleted file mode 100644 index 4d47c5fbd..000000000 --- a/skills/infrastructure/kubernetes-debugging/references/network-debugging.md +++ /dev/null @@ -1,63 +0,0 @@ -# Network Debugging Reference - -> **Scope**: Service resolution, DNS debugging, port-forwarding for local testing, and NetworkPolicy verification. Does NOT cover pod crash diagnosis or resource limits. -> **Version range**: Kubernetes 1.25+ (ephemeral containers assumed available) -> **Generated**: 2026-04-16 - ---- - -## Service Resolution -- pod cannot reach another service - -```bash -# Verify the service exists and has endpoints -kubectl get svc -n -kubectl get endpoints -n - -# Check if endpoints are populated (empty = no matching pods) -kubectl describe endpoints -n -``` - ---- - -## DNS Debugging - -```bash -# Run a DNS lookup from inside the cluster -kubectl run dns-debug --rm -it --restart=Never --image=busybox:1.36 -n -- \ - nslookup ..svc.cluster.local - -# Check CoreDNS pods are running -kubectl get pods -n kube-system -l k8s-app=kube-dns - -# Check CoreDNS logs for errors -kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50 -``` - ---- - -## Port-Forward for Local Testing - -```bash -# Forward a pod port to localhost -kubectl port-forward pod/ -n 8080:8080 - -# Forward a service port to localhost -kubectl port-forward svc/ -n 8080:80 - -# Test the endpoint locally -curl -v http://localhost:8080/healthz -``` - ---- - -## NetworkPolicy Blocking Traffic - -See kubernetes-security skill for full policy patterns. - -```bash -# List network policies in the namespace -kubectl get networkpolicy -n - -# Describe a specific policy to see ingress/egress rules -kubectl describe networkpolicy -n -``` diff --git a/skills/infrastructure/kubernetes-debugging/references/resource-debugging.md b/skills/infrastructure/kubernetes-debugging/references/resource-debugging.md deleted file mode 100644 index 4a2e5753b..000000000 --- a/skills/infrastructure/kubernetes-debugging/references/resource-debugging.md +++ /dev/null @@ -1,104 +0,0 @@ -# Resource Debugging Reference - -> **Scope**: CPU throttling detection, memory limits and OOMKill analysis, ephemeral storage pressure, ephemeral debug containers, and kubectl command reference table. Does NOT cover crash diagnosis root causes or networking. -> **Version range**: Kubernetes 1.25+ (ephemeral containers assumed available) -> **Generated**: 2026-04-16 - ---- - -## CPU Throttling -- container is being throttled by its CPU limit - -```bash -# Check current usage vs limits -kubectl top pod -n --containers - -# Check throttling metrics (requires metrics-server or Prometheus) -# Look for container_cpu_cfs_throttled_periods_total in metrics -kubectl describe pod -n | grep -A 5 "Limits" -``` - ---- - -## Memory Limits -- OOMKill or high memory pressure - -```bash -# Check if the pod was OOMKilled -kubectl get pod -n -o jsonpath='{.status.containerStatuses[0].lastState}' - -# Check current memory usage -kubectl top pod -n --containers - -# Check node memory pressure -kubectl describe node | grep -A 5 "Conditions" -``` - ---- - -## Ephemeral Storage Pressure -- logs or temp files fill the node disk - -```bash -# Check node disk pressure condition -kubectl describe node | grep -A 2 "DiskPressure" - -# Check pod ephemeral storage usage -kubectl describe pod -n | grep -A 3 "ephemeral-storage" -``` - ---- - -## Ephemeral Debug Containers - -When a container has no shell (distroless, scratch), use ephemeral containers: - -```bash -# Attach a debug container with networking tools -kubectl debug -it -n \ - --image=nicolaka/netshoot \ - --target= - -# Inside the debug container, you can: -# - Check network: curl, dig, tcpdump, ss -# - Inspect filesystem: ls /proc/1/root (target container's filesystem) -# - Check processes: ps aux -``` - -```bash -# Debug a node directly -kubectl debug node/ -it --image=ubuntu - -# This creates a pod with hostPID, hostNetwork, and mounts the node filesystem at /host -# Useful for: checking kubelet logs, node networking, disk usage -chroot /host -journalctl -u kubelet --no-pager --tail=50 -``` - ---- - -## kubectl Commands Reference - -| Command | Purpose | -|---------|---------| -| `kubectl describe pod -n ` | Full pod status, events, conditions | -| `kubectl logs -n --previous` | Logs from the last crashed container | -| `kubectl logs -n --tail=100 -f` | Stream last 100 lines and follow | -| `kubectl get events -n --sort-by='.lastTimestamp'` | Namespace events in chronological order | -| `kubectl top pod -n ` | CPU and memory usage per pod | -| `kubectl top nodes` | CPU and memory usage per node | -| `kubectl get pod -n -o yaml` | Full pod spec and status in YAML | -| `kubectl exec -it -n -- /bin/sh` | Shell into a running container | -| `kubectl port-forward pod/ -n 8080:8080` | Forward pod port to localhost | -| `kubectl debug pod/ -n --image=busybox -it` | Attach ephemeral debug container | -| `kubectl rollout status deployment/ -n ` | Watch deployment rollout progress | -| `kubectl rollout undo deployment/ -n ` | Roll back to previous revision | - ---- - -## Error Handling - -### Error: "kubectl exec" fails with "container not running" -Cause: The container is in CrashLoopBackOff or has not started yet. -Solution: Use `kubectl logs --previous` to get crash logs. If the container exits immediately, check the entrypoint command and environment variables. - -### Error: "kubectl debug" not available -Cause: Ephemeral containers require Kubernetes 1.25+ and the feature gate to be enabled. -Solution: Check cluster version with `kubectl version`. For older clusters, create a standalone debug pod in the same namespace with `kubectl run`. diff --git a/skills/infrastructure/kubernetes-security/SKILL.md b/skills/infrastructure/kubernetes-security/SKILL.md deleted file mode 100644 index 746ea747f..000000000 --- a/skills/infrastructure/kubernetes-security/SKILL.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -name: kubernetes-security -promoted_to: kubernetes -description: "Kubernetes security: RBAC, PodSecurity, network policies." -user-invocable: false -context: fork -agent: kubernetes-helm-engineer -routing: - triggers: - - "kubernetes security" - - "k8s RBAC" - - "RBAC setup" - - "pod security policy" - - "network policy" - category: kubernetes - pairs_with: - - kubernetes-debugging - - cobalt-core ---- - -# Kubernetes Security Skill - -Harden Kubernetes clusters and workloads through RBAC, pod security, network isolation, secret management, and supply chain controls. - -## Reference Loading Table - -| Signal | Reference | Size | -|--------|-----------|------| -| RBAC, Role, RoleBinding, ClusterRole, ServiceAccount, least-privilege, access control, permissions | `references/rbac-patterns.md` | ~60 lines | -| PodSecurity, SecurityContext, runAsNonRoot, readOnlyRootFilesystem, restricted, baseline, image hardening, distroless, Dockerfile | `references/pod-security.md` | ~90 lines | -| NetworkPolicy, default-deny, allow-list, egress, ingress, DNS, lateral movement, namespace isolation | `references/network-policies.md` | ~70 lines | -| cosign, Kyverno, OPA, admission controller, Sealed Secrets, External Secrets, supply chain, misconfiguration, privileged | `references/supply-chain.md` | ~120 lines | - -**Loading rule.** Read the references whose signals match the task before responding. - ---- - -## Phase 1: IDENTIFY - -Determine which security domain the user is asking about. - -| Domain | Reference | -|--------|-----------| -| Access control, permissions, roles | `references/rbac-patterns.md` | -| Pod hardening, container security | `references/pod-security.md` | -| Network isolation, traffic rules | `references/network-policies.md` | -| Image signing, secrets, admission control | `references/supply-chain.md` | - -If the question spans multiple domains, load all relevant references. Most production hardening tasks touch at least RBAC + pod security. - -**Gate**: Domain identified. Reference(s) loaded. Proceed to Phase 2. - ---- - -## Phase 2: RESPOND - -Use loaded reference knowledge to answer with concrete YAML manifests and specific configurations. The references contain complete, copy-paste-ready examples for each security domain. - -For general Kubernetes debugging, pair with the `kubernetes-debugging` skill. - -**Gate**: Question answered with reference-backed manifests, not generic advice. - ---- - -## Phase 3: VERIFY - -Validate the security posture against the misconfiguration table in `references/supply-chain.md`. Flag any of the 8 common misconfigurations if present in the user's manifests. - ---- - -## References - -- [Kubernetes Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) -- [Kubernetes Network Policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) -- [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) -- [External Secrets Operator](https://external-secrets.io/) -- [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) -- [Cosign](https://docs.sigstore.dev/cosign/overview/) -- [Kyverno](https://kyverno.io/) diff --git a/skills/infrastructure/kubernetes-security/references/network-policies.md b/skills/infrastructure/kubernetes-security/references/network-policies.md deleted file mode 100644 index a427a154b..000000000 --- a/skills/infrastructure/kubernetes-security/references/network-policies.md +++ /dev/null @@ -1,88 +0,0 @@ -# Network Policies - -> **Scope**: Default-deny NetworkPolicy YAML, allow-list patterns, DNS egress rules, and namespace isolation -> **Version range**: Kubernetes 1.26+ (NetworkPolicy v1 stable) -> **Generated**: 2026-04-16 — verify against current Kubernetes Network Policies documentation - ---- - -## Overview - -Start with a default-deny policy for both ingress and egress in every namespace. Apply this on day one, not later. Without network policies, lateral movement between compromised pods is trivial. - ---- - -## Default Deny All Traffic - -```yaml -# Default deny all traffic in the namespace -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: default-deny-all - namespace: production -spec: - podSelector: {} - policyTypes: - - Ingress - - Egress -``` - ---- - -## Allow-List: Frontend to Backend - -```yaml -# Allow frontend pods to reach backend on port 8080 -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: allow-frontend-to-backend - namespace: production -spec: - podSelector: - matchLabels: - app: backend - policyTypes: - - Ingress - ingress: - - from: - - podSelector: - matchLabels: - app: frontend - ports: - - protocol: TCP - port: 8080 -``` - ---- - -## DNS Egress (Required for Service Discovery) - -```yaml -# Allow DNS egress for all pods (required for service discovery) -apiVersion: networking.k8s.io/v1 -kind: NetworkPolicy -metadata: - name: allow-dns-egress - namespace: production -spec: - podSelector: {} - policyTypes: - - Egress - egress: - - to: [] - ports: - - protocol: UDP - port: 53 - - protocol: TCP - port: 53 -``` - ---- - -## Error Handling - -### Error: NetworkPolicy blocking legitimate traffic -Cause: Default-deny is in place but the allow-list rule is missing or has incorrect label selectors. -Solution: Verify pod labels match the NetworkPolicy `podSelector` and `from`/`to` selectors. Use `kubectl describe networkpolicy` to inspect rules. diff --git a/skills/infrastructure/kubernetes-security/references/pod-security.md b/skills/infrastructure/kubernetes-security/references/pod-security.md deleted file mode 100644 index 208adef4e..000000000 --- a/skills/infrastructure/kubernetes-security/references/pod-security.md +++ /dev/null @@ -1,98 +0,0 @@ -# Pod Security - -> **Scope**: PodSecurityStandards (Baseline, Restricted, Privileged), SecurityContext configuration, non-root enforcement, and image hardening -> **Version range**: Kubernetes 1.25+ (PodSecurity admission GA, PodSecurityPolicy removed) -> **Generated**: 2026-04-16 — verify against current Kubernetes Pod Security Standards documentation - ---- - -## Overview - -Kubernetes PodSecurity admission replaces the deprecated PodSecurityPolicy. Apply labels at the namespace level. All containers must run as non-root with a read-only root filesystem unless there is a documented exception. If an app claims it needs root, it usually just needs a writable `/tmp`, which an emptyDir volume solves. - ---- - -## Namespace-Level PodSecurity Labels - -```yaml -# Enforce restricted profile, warn on baseline violations -apiVersion: v1 -kind: Namespace -metadata: - name: production - labels: - pod-security.kubernetes.io/enforce: restricted - pod-security.kubernetes.io/warn: restricted - pod-security.kubernetes.io/audit: restricted -``` - ---- - -## Restricted-Compliant Pod SecurityContext - -```yaml -apiVersion: v1 -kind: Pod -metadata: - name: secure-app -spec: - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - containers: - - name: app - image: registry.example.com/app:v1.2.3@sha256:abc123 - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - runAsUser: 1000 - runAsGroup: 1000 - capabilities: - drop: ["ALL"] - resources: - limits: - memory: "256Mi" - cpu: "500m" - requests: - memory: "128Mi" - cpu: "100m" -``` - ---- - -## PodSecurity Levels - -- **Baseline** — blocks known privilege escalations (hostNetwork, privileged, hostPID) but allows running as root -- **Restricted** — enforces non-root, drops all capabilities, requires seccomp profile, disallows privilege escalation - ---- - -## Image Hardening - -Containers should never run as privileged or with elevated capabilities unless explicitly justified. Privileged mode grants full host access to an attacker if the pod is compromised. Use specific capabilities or debug containers instead. - -Build minimal, non-root container images: - -```dockerfile -# Use distroless or minimal base images -FROM gcr.io/distroless/static-debian12:nonroot -COPY --chown=65532:65532 app /app -USER 65532:65532 -ENTRYPOINT ["/app"] -``` - -Requirements: -- **Non-root user**: Always set `USER` in the Dockerfile and `runAsNonRoot: true` in the SecurityContext -- **Read-only root filesystem**: Use `readOnlyRootFilesystem: true` and mount writable volumes only where needed -- **Distroless or scratch**: No shell, no package manager — reduces attack surface -- **Pin image digests**: Use `image:tag@sha256:...` to prevent tag mutation attacks -- **Scan images**: Run Trivy, Grype, or Snyk in CI before pushing to registry - ---- - -## Error Handling - -### Error: Pod rejected by PodSecurity admission -Cause: Pod spec violates the namespace's PodSecurity level (e.g., missing `runAsNonRoot`, `privileged: true`). -Solution: Check the admission warning message, then update the pod's SecurityContext to comply with the enforced level. diff --git a/skills/infrastructure/kubernetes-security/references/rbac-patterns.md b/skills/infrastructure/kubernetes-security/references/rbac-patterns.md deleted file mode 100644 index d0c501d36..000000000 --- a/skills/infrastructure/kubernetes-security/references/rbac-patterns.md +++ /dev/null @@ -1,65 +0,0 @@ -# RBAC Patterns - -> **Scope**: Role, RoleBinding, ClusterRole YAML manifests and ServiceAccount best practices for least-privilege Kubernetes access control -> **Version range**: Kubernetes 1.26+ (RBAC v1 stable) -> **Generated**: 2026-04-16 — verify against current Kubernetes RBAC documentation - ---- - -## Overview - -RBAC (Role-Based Access Control) is the primary authorization mechanism in Kubernetes. Grant the minimum permissions required. Prefer namespace-scoped Roles over ClusterRoles. Write exact verbs and resources in production. Even in dev clusters, because dev habits carry forward and dev manifests get promoted. Write exact verbs and resources every time. - ---- - -## Namespace-Scoped Role - -```yaml -# Good: namespace-scoped Role with specific verbs and resources -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - namespace: app-team - name: deployment-reader -rules: - - apiGroups: ["apps"] - resources: ["deployments"] - verbs: ["get", "list", "watch"] -``` - ---- - -## RoleBinding - -```yaml -# Bind the Role to a specific ServiceAccount, not a user or group wildcard -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - namespace: app-team - name: deployment-reader-binding -subjects: - - kind: ServiceAccount - name: ci-deployer - namespace: app-team -roleRef: - kind: Role - name: deployment-reader - apiGroup: rbac.authorization.k8s.io -``` - ---- - -## ServiceAccount Best Practices - -- Create dedicated ServiceAccounts per workload -- Set `automountServiceAccountToken: false` on pods that have no need for Kubernetes API access -- Regularly audit which ServiceAccounts have ClusterRole bindings - ---- - -## Error Handling - -### Error: RBAC "access denied" errors in application logs -Cause: ServiceAccount lacks required permissions. -Solution: Identify the API group, resource, and verb from the error message. Create or update a Role with the exact permissions needed. List specific verbs and resources. diff --git a/skills/infrastructure/kubernetes-security/references/supply-chain.md b/skills/infrastructure/kubernetes-security/references/supply-chain.md deleted file mode 100644 index 0ccc17e3c..000000000 --- a/skills/infrastructure/kubernetes-security/references/supply-chain.md +++ /dev/null @@ -1,119 +0,0 @@ -# Supply Chain Security - -> **Scope**: Image signing with cosign, admission controllers (Kyverno, OPA Gatekeeper), secret management (Sealed Secrets, External Secrets Operator), and common misconfiguration detection -> **Version range**: cosign v2+, Kyverno v1.10+, External Secrets Operator v0.9+ -> **Generated**: 2026-04-16 — verify against current tool documentation - ---- - -## Overview - -Supply chain security covers image provenance, admission-time policy enforcement, secret management, and misconfiguration detection. These controls complement RBAC, pod security, and network policies by securing the software delivery pipeline and runtime configuration. - ---- - -## Image Signing with cosign - -```bash -# Sign an image after building -cosign sign --key cosign.key registry.example.com/app:v1.2.3@sha256:abc123 - -# Verify before deploying -cosign verify --key cosign.pub registry.example.com/app:v1.2.3@sha256:abc123 -``` - ---- - -## Admission Controllers - -Enforce policy at deploy time: -- **Kyverno** or **OPA Gatekeeper** — reject pods that violate security policies -- **Sigstore Policy Controller** — verify image signatures before admission - -### Kyverno Policy: Require Non-Root Containers - -```yaml -apiVersion: kyverno.io/v1 -kind: ClusterPolicy -metadata: - name: require-run-as-nonroot -spec: - validationFailureAction: Enforce - rules: - - name: run-as-non-root - match: - any: - - resources: - kinds: - - Pod - validate: - message: "Containers must run as non-root" - pattern: - spec: - containers: - - securityContext: - runAsNonRoot: true -``` - ---- - -## Secret Management - -Store secrets using Sealed Secrets or External Secrets Operator, not environment variables from manifests or checked-in YAML. Secrets exposed as env vars are visible in `kubectl describe pod` output, which makes them trivially discoverable after any pod compromise. - -### Sealed Secrets - -Encrypts secrets client-side so they are safe in Git: - -```bash -# Encrypt a secret with kubeseal -kubectl create secret generic db-creds \ - --from-literal=password=supersecret \ - --dry-run=client -o yaml | \ - kubeseal --format yaml > sealed-db-creds.yaml -``` - -### External Secrets Operator - -Syncs secrets from external vaults: - -```yaml -apiVersion: external-secrets.io/v1beta1 -kind: ExternalSecret -metadata: - name: db-credentials - namespace: production -spec: - refreshInterval: 1h - secretStoreRef: - name: vault-backend - kind: ClusterSecretStore - target: - name: db-credentials - data: - - secretKey: password - remoteRef: - key: secret/data/production/db - property: password -``` - -### Practices to Detect and Fix - -- Mounting secrets as environment variables in the pod spec (visible in `kubectl describe pod`) -- Storing secrets in ConfigMaps -- Hardcoding credentials in container images or Dockerfiles - ---- - -## Common Misconfiguration Detection - -| Misconfiguration | Risk | Fix | -|------------------|------|-----| -| `privileged: true` | Full host access | Remove or use specific capabilities | -| `hostNetwork: true` | Pod shares host network stack | Use CNI networking | -| `hostPID: true` / `hostIPC: true` | Can see/signal host processes | Remove unless debugging | -| Wildcard RBAC verbs (`*`) | Grants all operations | List specific verbs | -| `automountServiceAccountToken: true` on workloads | Token exposed to compromised pod | Set to `false` unless API access needed | -| No resource limits | Pod can exhaust node resources (DoS) | Set CPU and memory limits | -| Latest tag without digest | Image can change without notice | Pin by digest | -| Secrets as env vars in pod spec | Visible in `kubectl describe` | Mount as files or use external secrets | diff --git a/skills/infrastructure/kubernetes/SKILL.md b/skills/infrastructure/kubernetes/SKILL.md index fc56c804b..321c581d1 100644 --- a/skills/infrastructure/kubernetes/SKILL.md +++ b/skills/infrastructure/kubernetes/SKILL.md @@ -49,7 +49,7 @@ Kubernetes debugging, security hardening, and infrastructure tooling. Covers pod | PodSecurity, SecurityContext, runAsNonRoot, readOnlyRootFilesystem, restricted, baseline | `references/pod-security.md` | ~90 lines | | NetworkPolicy, default-deny, allow-list, namespace isolation | `references/network-policies.md` | ~70 lines | | cosign, Kyverno, OPA, admission controller, Sealed Secrets, External Secrets | `references/supply-chain.md` | ~120 lines | -| kvm-exporter, libvirt, hypervisor, collector, scrape, steal time, NUMA | `references/kvm-exporter.md` | ~800 lines | +| kvm-exporter, metrics, prometheus, libvirt, hypervisor, collector, scrape, steal time, NUMA, cgroups, cloud hypervisor | `references/cobalt-kvm-exporter.md` | ~800 lines | | cobaltcore concurrency, goroutine, semaphore, TryLock | `references/cobalt-concurrency-patterns.md` | ~200 lines | | cobaltcore testing, mock, moq, Kind cluster | `references/cobalt-testing-patterns.md` | ~200 lines | | kubernetes debugging process, triage flow, diagnosis routing | `references/kubernetes-debugging.md` | ~50 lines | diff --git a/skills/infrastructure/kubernetes/references/cobalt-core.md b/skills/infrastructure/kubernetes/references/cobalt-core.md index 2dc61ba99..d3d8709f4 100644 --- a/skills/infrastructure/kubernetes/references/cobalt-core.md +++ b/skills/infrastructure/kubernetes/references/cobalt-core.md @@ -8,6 +8,10 @@ Domain knowledge for the cobaltcore-dev project family — SAP Converged Cloud i |-----------|-----------|-----------| | KVM Exporter | `cobaltcore-dev/kvm-exporter` | `cobalt-kvm-exporter.md` | +If the component is not listed, tell the user no reference exists yet and offer to analyze the repo (see Extension Process). + +The references contain: architecture and data flow diagrams; complete metric catalogs with types, labels, and descriptions; configuration options and environment variables; deployment models (Helm, DaemonSet, container specs); code patterns (concurrency, caching, error handling); testing strategies (unit mocks, E2E with Kind clusters); alerting rules and operational concerns. + ## Implementation Pairing - Go code patterns: pair with `go-patterns` skill @@ -20,3 +24,6 @@ To add a new cobaltcore repo: 1. Analyze repo systematically (README, go.mod, source, Dockerfile, Helm) 2. Create reference file at `references/cobalt-{repo-name}.md` 3. Update the Reference Loading Table in SKILL.md +4. Update the Component Table above + +Follow the structure established in `cobalt-kvm-exporter.md` for consistency. diff --git a/skills/infrastructure/kubernetes/references/kubernetes-debugging.md b/skills/infrastructure/kubernetes/references/kubernetes-debugging.md index b8f6a914c..d57cb3287 100644 --- a/skills/infrastructure/kubernetes/references/kubernetes-debugging.md +++ b/skills/infrastructure/kubernetes/references/kubernetes-debugging.md @@ -4,7 +4,9 @@ Systematic diagnosis of pod failures, networking issues, and resource problems u ## Triage Flow -Follow this sequence for every pod or workload issue. Do not skip steps — many failures are only visible in events and describe output, not in logs. +Follow this sequence for every pod or workload issue. Do not skip steps — many failures (scheduling, image pull, volume mount) are only visible in events and describe output, not in logs, so jumping straight to logs misses them. + +Always specify `-n ` explicitly in every command; never rely on the default context namespace, because the wrong namespace silently returns empty or misleading results. ```bash # 1. Overview @@ -14,6 +16,8 @@ kubectl describe pod -n # 3. Current logs kubectl logs -n -c # 4. Previous logs (critical for CrashLoopBackOff) +# Always check --previous before current logs for crashed containers, +# because deleting or restarting the pod destroys these logs permanently. kubectl logs -n -c --previous # 5. Namespace events kubectl get events -n --sort-by='.lastTimestamp' @@ -21,6 +25,8 @@ kubectl get events -n --sort-by='.lastTimestamp' kubectl exec -it -n -c -- /bin/sh ``` +Use read-only commands (describe, logs, get) to gather evidence before proposing any modifications. Never suggest changes based on assumptions — gather diagnostic output first. + ## Diagnosis Routing | Symptom | Reference | diff --git a/skills/infrastructure/kubernetes/references/kubernetes-security.md b/skills/infrastructure/kubernetes/references/kubernetes-security.md index bbb0852f8..4d250859a 100644 --- a/skills/infrastructure/kubernetes/references/kubernetes-security.md +++ b/skills/infrastructure/kubernetes/references/kubernetes-security.md @@ -13,4 +13,19 @@ Harden Kubernetes clusters and workloads through RBAC, pod security, network iso If the question spans multiple domains, load all relevant references. Most production hardening touches at least RBAC + pod security. +## Process + +1. **RESPOND** — answer with concrete YAML manifests and specific configurations from the loaded references; they contain complete, copy-paste-ready examples for each security domain. Reference-backed manifests, not generic advice. +2. **VERIFY** — validate the security posture against the misconfiguration table in `supply-chain.md`. Flag any of the 8 common misconfigurations present in the user's manifests. + For general Kubernetes debugging, see `kubernetes-debugging.md`. + +## External References + +- [Kubernetes Pod Security Standards](https://kubernetes.io/docs/concepts/security/pod-security-standards/) +- [Kubernetes Network Policies](https://kubernetes.io/docs/concepts/services-networking/network-policies/) +- [Kubernetes RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) +- [External Secrets Operator](https://external-secrets.io/) +- [Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) +- [Cosign](https://docs.sigstore.dev/cosign/overview/) +- [Kyverno](https://kyverno.io/) diff --git a/skills/infrastructure/kubernetes/references/network-debugging.md b/skills/infrastructure/kubernetes/references/network-debugging.md index 4d47c5fbd..5898c9c87 100644 --- a/skills/infrastructure/kubernetes/references/network-debugging.md +++ b/skills/infrastructure/kubernetes/references/network-debugging.md @@ -52,7 +52,7 @@ curl -v http://localhost:8080/healthz ## NetworkPolicy Blocking Traffic -See kubernetes-security skill for full policy patterns. +See kubernetes-security.md for full policy patterns. ```bash # List network policies in the namespace diff --git a/skills/infrastructure/public-web-deploy/SKILL.md b/skills/infrastructure/public-web-deploy/SKILL.md index 73be8e857..01fc31bd8 100644 --- a/skills/infrastructure/public-web-deploy/SKILL.md +++ b/skills/infrastructure/public-web-deploy/SKILL.md @@ -35,7 +35,7 @@ routing: pairs_with: - shell-process-patterns - service-health-check - - kubernetes-security + - kubernetes --- # Public Web Deploy Skill diff --git a/skills/infrastructure/service-health-check/SKILL.md b/skills/infrastructure/service-health-check/SKILL.md index 92ea42efe..f74dc26fe 100644 --- a/skills/infrastructure/service-health-check/SKILL.md +++ b/skills/infrastructure/service-health-check/SKILL.md @@ -30,7 +30,7 @@ routing: - "security feed audit" category: infrastructure pairs_with: - - kubernetes-debugging + - kubernetes - condition-based-waiting - e2e-testing --- diff --git a/skills/infrastructure/shell-config/references/fish-shell-config.md b/skills/infrastructure/shell-config/references/fish-shell-config.md index 3a2cb72d5..e5d0d8213 100644 --- a/skills/infrastructure/shell-config/references/fish-shell-config.md +++ b/skills/infrastructure/shell-config/references/fish-shell-config.md @@ -7,10 +7,10 @@ Fish is not POSIX. Every pattern here targets Fish 3.0+ (supports `$()`, `&&`, ` | Signal | Load These Files | Why | |---|---|---| -| migrations | `bash-migration.md` | Loads detailed guidance from `bash-migration.md`. | -| editing fish config: variable assignment, PATH management, conditionals, tool integration | `fish-preferred-patterns.md` | Loads detailed guidance from `fish-preferred-patterns.md`. | +| migrating from Bash: converting `.bashrc`/`.bash_aliases`, `source`, `export`, `[[` in Fish file | `fish-bash-migration.md` | Full Bash-to-Fish syntax translation table. | +| editing fish config: variable assignment, PATH management, conditionals; error audit: "unknown command", PATH not persisting, abbreviation not working, broken conf.d | `fish-preferred-patterns.md` | Failure mode catalog with detection commands and error-fix mappings. | | syntax lookup: variable scope, PATH, functions, abbreviations, conditionals, debugging | `fish-quick-reference.md` | Loads detailed guidance from `fish-quick-reference.md`. | -| wiring dev tools into fish: Go, Rust, Node, Python, Docker, PATH and init hooks | `tool-integrations.md` | Loads detailed guidance from `tool-integrations.md`. | +| wiring dev tools into fish: Go, Rust, Node, Python, Docker, PATH and init hooks | `fish-tool-integrations.md` | Loads detailed guidance from `fish-tool-integrations.md`. | ## Instructions @@ -230,3 +230,18 @@ Solution: Use a function instead. Move the logic from `abbr` to a file in `funct Cause: Missing `-x` (export) flag on `set` Solution: Use `set -gx VAR value` to make variable visible to subprocesses. Check with `set --show VAR` to inspect current scope and export status. +## Worked Examples + +### Setting Up a New Fish Config +User says: "Set up my Fish shell config" +1. Confirm Fish context +2. Create modular structure in `~/.config/fish/` +3. Write `conf.d/00-path.fish`, `conf.d/10-env.fish`, `conf.d/20-abbreviations.fish` +4. Syntax-check all files + +### Migrating a Bash Alias File +User says: "Convert my .bash_aliases to Fish" +1. Read `.bash_aliases`, confirm Fish target +2. Determine which become abbreviations vs functions +3. Write abbreviations to `conf.d/`, functions to `functions/` +4. Syntax-check, test in clean shell diff --git a/skills/infrastructure/shell-config/references/zsh-preferred-patterns.md b/skills/infrastructure/shell-config/references/zsh-preferred-patterns.md index 8416bc142..399b80f11 100644 --- a/skills/infrastructure/shell-config/references/zsh-preferred-patterns.md +++ b/skills/infrastructure/shell-config/references/zsh-preferred-patterns.md @@ -244,6 +244,7 @@ grep -n '^autoload ' ~/.zshrc 2>/dev/null | grep -v '\-U' | `compinit: insecure directories` | World/group-writable fpath entries | `compaudit`; `chmod go-w /path`; or `compinit -u` | | `add-zsh-hook: command not found` | `autoload -Uz add-zsh-hook` not called | Add `autoload -Uz add-zsh-hook` before hook calls | | `~/.zcompdump` outdated | Stale cache after adding new completions | `rm ~/.zcompdump && compinit` | +| `zsh -n` reports syntax error in valid-looking code | `EXTENDED_GLOB` patterns like `^` or `#` in unquoted strings parsed as glob operators | Quote strings containing these characters; `setopt NO_EXTENDED_GLOB` temporarily to isolate | --- diff --git a/skills/infrastructure/shell-config/references/zsh-shell-config.md b/skills/infrastructure/shell-config/references/zsh-shell-config.md index e77fadfc9..10b9ede67 100644 --- a/skills/infrastructure/shell-config/references/zsh-shell-config.md +++ b/skills/infrastructure/shell-config/references/zsh-shell-config.md @@ -7,10 +7,10 @@ Zsh is POSIX-compatible with powerful extensions. Every pattern here targets Zsh | Signal | Load These Files | Why | |---|---|---| -| migrations, bash syntax, POSIX shell | `bash-migration.md` | Bash→Zsh syntax translation table with code blocks for each pattern | +| migrations, bash syntax, POSIX shell | `zsh-bash-migration.md` | Bash→Zsh syntax translation table with code blocks for each pattern | | parameter expansion, scoping, special vars, fpath | `zsh-quick-reference.md` | Variable flags, expansion flags `${(f)}` `${(s)}` `${(j)}`, special variables | | implementation patterns, failure modes, detection commands | `zsh-preferred-patterns.md` | Pattern catalog with grep detection and error-fix mappings | -| Go, Rust, Node, Python, starship, direnv, fzf, zoxide, mise | `tool-integrations.md` | Concrete zsh integration patterns for common dev tools | +| Go, Rust, Node, Python, starship, direnv, fzf, zoxide, mise | `zsh-tool-integrations.md` | Concrete zsh integration patterns for common dev tools | ## Step 1: Confirm Zsh Context @@ -20,7 +20,7 @@ Before writing any shell code, confirm the target is Zsh: - Target file has `.zsh` extension, or - Target file is one of: `.zshrc`, `.zshenv`, `.zprofile`, `.zlogin`, `.zlogout` -If none hold, this skill does not apply — route to fish-shell-config or bash. +If none hold, this skill does not apply — load fish-shell-config.md instead, or stop for bash. ## Step 2: Choose the Correct RC File @@ -299,12 +299,12 @@ zsh -i -c 'zmodload zsh/zprof; compinit; zprof' 2>&1 | head -20 | Task Signal | Load | Why | |-------------|------|-----| -| Migrating from Bash, converting `.bashrc`, `export VAR=`, `[[ ]]`, arrays, heredocs | `bash-migration.md` | Full Bash→Zsh syntax translation table | +| Migrating from Bash, converting `.bashrc`, `export VAR=`, `[[ ]]`, arrays, heredocs | `zsh-bash-migration.md` | Full Bash→Zsh syntax translation table | | Variable scoping, `typeset` flags, `${(f)}` `${(s:,:)}` expansion, `$REPLY`, `$MATCH` | `zsh-quick-reference.md` | Parameter expansion flags, special variables, control flow cheatsheet | | Error audit, PATH not persisting, completions slow, glob errors, hook not firing | `zsh-preferred-patterns.md` | Failure modes with grep detection commands and error-fix mappings | -| Go, Rust, Docker, Node.js, Python, pyenv, fnm, starship, direnv, fzf, zoxide, mise | `tool-integrations.md` | Concrete integration patterns for common dev tools | +| Go, Rust, Docker, Node.js, Python, pyenv, fnm, starship, direnv, fzf, zoxide, mise | `zsh-tool-integrations.md` | Concrete integration patterns for common dev tools | -- `${CLAUDE_SKILL_DIR}/references/bash-migration.md`: Complete Bash-to-Zsh syntax translation table +- `${CLAUDE_SKILL_DIR}/references/zsh-bash-migration.md`: Complete Bash-to-Zsh syntax translation table - `${CLAUDE_SKILL_DIR}/references/zsh-quick-reference.md`: Variable scoping, parameter expansion flags, and special variables - `${CLAUDE_SKILL_DIR}/references/zsh-preferred-patterns.md`: Failure mode catalog with grep detection commands and error-fix mappings -- `${CLAUDE_SKILL_DIR}/references/tool-integrations.md`: Concrete integration patterns for Go, Rust, Docker, Node.js, Python, and shell enhancers +- `${CLAUDE_SKILL_DIR}/references/zsh-tool-integrations.md`: Concrete integration patterns for Go, Rust, Docker, Node.js, Python, and shell enhancers diff --git a/skills/infrastructure/shell-process-patterns/SKILL.md b/skills/infrastructure/shell-process-patterns/SKILL.md index 660758bab..907a67048 100644 --- a/skills/infrastructure/shell-process-patterns/SKILL.md +++ b/skills/infrastructure/shell-process-patterns/SKILL.md @@ -55,7 +55,7 @@ Start, supervise, and terminate shell processes safely -- background jobs, subsh - Cron scripts and scheduled job reliability (owned by `cron-job-auditor`). - Polling, retry, backoff, health-check loops (owned by `condition-based-waiting`). - Service health reporting (owned by `service-health-check`). -- Fish shell configuration (owned by `fish-shell-config`). +- Fish shell configuration (owned by `shell-config`). - Shell language features unrelated to process lifecycle -- parameter expansion, arrays, etc. ## Reference Loading Table diff --git a/skills/infrastructure/zsh-shell-config/SKILL.md b/skills/infrastructure/zsh-shell-config/SKILL.md deleted file mode 100644 index 2bba8e34e..000000000 --- a/skills/infrastructure/zsh-shell-config/SKILL.md +++ /dev/null @@ -1,389 +0,0 @@ ---- -name: zsh-shell-config -promoted_to: shell-config -description: "Zsh shell configuration, PATH management, completions, and framework setup." -user-invocable: false -allowed-tools: - - Read - - Write - - Bash - - Grep - - Glob - - Edit -routing: - category: process - not_for: "not for non-zsh shells — only fires for Zsh configuration" - triggers: - - zsh - - zsh shell - - .zshrc - - zshrc - - .zshenv - - zshenv - - .zprofile - - zprofile - - compinit - - fpath - - autoload -Uz - - oh-my-zsh - - prezto - - zinit - - antigen - - p10k - - powerlevel10k - - setopt - - zsh completion - - zsh function - - zsh plugin - - typeset -U - - zplug - - configure zsh - - zsh config - pairs_with: [] - force_route: true ---- - -# Zsh Shell Configuration Skill - -Zsh is POSIX-compatible with powerful extensions. Every pattern here targets Zsh 5.8+ (ships with macOS 14+, Ubuntu 22.04+). All generated code must use Zsh-native syntax — `typeset`/`local` for scoping, `path` array for PATH, `compinit` for completions. - -## Reference Loading Table - -| Signal | Load These Files | Why | -|---|---|---| -| migrations, bash syntax, POSIX shell | `bash-migration.md` | Bash→Zsh syntax translation table with code blocks for each pattern | -| parameter expansion, scoping, special vars, fpath | `zsh-quick-reference.md` | Variable flags, expansion flags `${(f)}` `${(s)}` `${(j)}`, special variables | -| implementation patterns, failure modes, detection commands | `zsh-preferred-patterns.md` | Pattern catalog with grep detection and error-fix mappings | -| Go, Rust, Node, Python, starship, direnv, fzf, zoxide, mise | `tool-integrations.md` | Concrete zsh integration patterns for common dev tools | - -## Step 1: Confirm Zsh Context - -Before writing any shell code, confirm the target is Zsh: - -- `$SHELL` contains `zsh`, or -- Target file has `.zsh` extension, or -- Target file is one of: `.zshrc`, `.zshenv`, `.zprofile`, `.zlogin`, `.zlogout` - -If none hold, this skill does not apply — route to fish-shell-config or bash. - -## Step 2: Choose the Correct RC File - -Zsh sources startup files in a defined order depending on shell type (login vs. interactive). - -**Load order**: - -| File | Sourced when | Use for | -|------|-------------|---------| -| `~/.zshenv` | Every zsh invocation (login, interactive, script) | `$PATH`, `$EDITOR`, `$LANG` — variables needed by all processes | -| `~/.zprofile` | Login shells only (before `.zshrc`) | Login-time setup (e.g., `eval "$(brew shellenv)"`) | -| `~/.zshrc` | Interactive shells only | Aliases, functions, completions, prompt, key bindings | -| `~/.zlogin` | Login shells (after `.zshrc`) | Rarely used; message-of-the-day type hooks | -| `~/.zlogout` | Login shell exit | Cleanup on logout | - -**Decision tree**: - -| What you're writing | Where it goes | -|---------------------|---------------| -| `PATH`, `EDITOR`, `LANG`, `GOPATH` | `~/.zshenv` | -| Homebrew / nix / login-time init | `~/.zprofile` | -| Aliases, functions, `compinit`, prompt | `~/.zshrc` | -| Key bindings (`bindkey`) | `~/.zshrc` | -| `setopt` / `unsetopt` | `~/.zshrc` | -| Framework (oh-my-zsh, prezto) | `~/.zshrc` | - -Place env vars in `~/.zshenv` not `~/.zshrc` — scripts and non-interactive shells source `.zshenv` only, so exported variables must live there to reach child processes started from scripts. - -## Step 3: Manage PATH - -Zsh uses `path` as an array tied to `$PATH`. Use `typeset -U path` to deduplicate automatically. - -```zsh -# ~/.zshenv — PATH management -typeset -U path # Enforce unique entries; run once at top of file - -path=( - ~/bin - ~/.local/bin - $path # Preserve existing entries -) -export PATH # Re-export after array modification -``` - -**fpath and autoload**: - -```zsh -# Add completion directories to fpath before compinit -typeset -U fpath -fpath=(~/.zsh/completions $fpath) - -# Autoload functions from fpath -autoload -Uz compinit # -U: no alias expansion; -z: zsh-style autoload -autoload -Uz add-zsh-hook -autoload -Uz vcs_info - -# Completion cache: only re-init once per day -if [[ -n ~/.zcompdump(#qN.mh+24) ]]; then - compinit -else - compinit -C # -C: skip security check, use cached dump -fi -``` - -`~/.zcompdump` caches completion definitions. Without the age guard, `compinit` rescans all fpath entries on every shell start — adds 100–300ms of startup time. - -## Step 4: Write Variables and Functions - -**Variable scoping**: - -```zsh -local var="value" # Local to current function -typeset -i count=0 # Integer -typeset -l lower="VALUE" # Auto-lowercased -typeset -u upper="value" # Auto-uppercased -typeset -r CONST="fixed" # Read-only -typeset -x EXPORTED="val" # Export (same as export) -typeset -a arr # Indexed array -typeset -A assoc # Associative array -``` - -**Functions**: - -```zsh -# Regular function -mkcd() { - mkdir -p "$1" && cd "$1" -} - -# Alternative (preferred in zsh): function keyword -function mkcd { - mkdir -p "$1" && cd "$1" -} - -# Autoloaded function (place in a fpath directory as a standalone file) -# ~/.zsh/functions/mkcd — file contains only the function body: -# mkdir -p "$1" && cd "$1" -# Then in .zshrc: autoload -Uz mkcd -``` - -For autoloaded functions, the file must be named exactly after the function and live in a `fpath` directory. - -## Step 5: Completions - -```zsh -# ~/.zshrc — completions setup - -# 1. Extend fpath before compinit -fpath=(~/.zsh/completions $fpath) - -# 2. Initialize (with cache guard) -autoload -Uz compinit -compinit - -# 3. Key completion options -setopt COMPLETE_IN_WORD # Complete from both ends of word -setopt ALWAYS_TO_END # Move cursor to end after completion -setopt AUTO_MENU # Show menu on second Tab -setopt MENU_COMPLETE # Auto-select first match -zstyle ':completion:*' menu select - -# 4. Styling completions -zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' # Case-insensitive -zstyle ':completion:*' list-colors "${(s.:.)LS_COLORS}" -zstyle ':completion:*:descriptions' format '%B%d%b' -zstyle ':completion:*:warnings' format 'No matches for: %d' - -# 5. Add a completion function for a custom command -# ~/.zsh/completions/_mycommand -# #compdef mycommand -# _mycommand() { -# local -a commands -# commands=('start:Start the service' 'stop:Stop the service') -# _describe 'commands' commands -# } -``` - -## Step 6: Framework Awareness - -Detect active framework before adding configuration — frameworks own the prompt, completion init, and plugin loading. - -**Detection patterns**: - -```zsh -# oh-my-zsh -[[ -n "$ZSH" ]] && echo "oh-my-zsh active (ZSH=$ZSH)" -[[ -f ~/.oh-my-zsh/oh-my-zsh.sh ]] && echo "oh-my-zsh installed" - -# prezto -[[ -d ~/.zprezto ]] && echo "prezto installed" - -# zinit -[[ -d ~/.zinit ]] || [[ -d ~/.local/share/zinit ]] && echo "zinit installed" - -# antigen -command -v antigen &>/dev/null && echo "antigen active" - -# powerlevel10k / p10k -[[ -f ~/.p10k.zsh ]] && echo "p10k config present" -``` - -**Framework coexistence rules**: - -| Situation | Action | -|-----------|--------| -| oh-my-zsh active | Place custom config in `~/.oh-my-zsh/custom/` — not directly in `.zshrc` | -| prezto active | Use `~/.zpreztorc` modules; add extra config after `source ~/.zprezto/init.zsh` | -| zinit active | Add plugins with `zinit light`; place after zinit bootstrap block | -| p10k active | Call `[[ -f ~/.p10k.zsh ]] && source ~/.p10k.zsh` after framework init | -| No framework | Full control — structure `.zshrc` with `compinit`, prompt, plugins manually | - -When a framework manages `compinit`, do not call `compinit` again — double-init causes duplicate completion definitions and slows startup. - -## Step 7: Hooks - -Zsh hooks fire at defined shell events. Use `add-zsh-hook` to attach — it preserves existing handlers for the same event. - -```zsh -autoload -Uz add-zsh-hook - -# precmd: runs before each prompt (use for vcs_info, title updates) -_my_precmd() { - print -Pn "\e]0;%~\a" # Set terminal title to current dir -} -add-zsh-hook precmd _my_precmd - -# preexec: runs before each command (receives command string as $1) -_my_preexec() { - print -Pn "\e]0;$1\a" # Set terminal title to running command -} -add-zsh-hook preexec _my_preexec - -# chpwd: runs when directory changes -_my_chpwd() { - ls --color=auto # Auto-list on cd -} -add-zsh-hook chpwd _my_chpwd - -# zshaddhistory: runs before adding command to history; return 1 to suppress -_no_history() { - [[ "$1" == " "* ]] && return 1 # Leading space = skip history - return 0 -} -add-zsh-hook zshaddhistory _no_history -``` - -Writing `precmd() { ... }` directly overwrites any previously defined `precmd` — `add-zsh-hook` appends to the hook array instead. - -## Step 8: Zsh Options - -```zsh -# History -setopt HIST_IGNORE_DUPS # Ignore consecutive duplicates -setopt HIST_IGNORE_SPACE # Ignore commands starting with space -setopt HIST_EXPIRE_DUPS_FIRST # Expire duplicates first when trimming -setopt HIST_FIND_NO_DUPS # No dups in history search -setopt SHARE_HISTORY # Share history across sessions -setopt APPEND_HISTORY # Append rather than overwrite -setopt INC_APPEND_HISTORY # Write immediately, not on exit -HISTFILE=~/.zsh_history -HISTSIZE=50000 -SAVEHIST=50000 - -# Globbing -setopt EXTENDED_GLOB # Enable (#q...) glob qualifiers and ^ ~ -setopt NULL_GLOB # No-match globs expand to empty (no error) -setopt GLOB_DOTS # Dot files matched by * patterns - -# Behavior -setopt PIPE_FAIL # Pipeline exit code = first failed command -setopt NO_CLOBBER # Prevent > from overwriting files -setopt AUTO_CD # cd by typing directory name alone -setopt CORRECT # Suggest corrections for mistyped commands -setopt NO_BEEP # Silence - -# Key options to unset -unsetopt BG_NICE # Background jobs run at normal priority -unsetopt FLOW_CONTROL # Disable Ctrl+S/Ctrl+Q flow control -``` - -**Critical options**: - -| Option | Effect | Recommend | -|--------|--------|-----------| -| `PIPE_FAIL` | Pipeline returns exit code of first failed command | Always set | -| `NULL_GLOB` | Unmatched globs expand to empty list | Set to avoid "no matches" errors | -| `EXTENDED_GLOB` | Enables `^`, `~`, `(#q...)` qualifiers | Set when using glob qualifiers | -| `HIST_IGNORE_SPACE` | Commands prefixed with space skip history | Set for sensitive commands | -| `SHARE_HISTORY` | Multiple sessions share history in real time | Set for multi-terminal workflows | - -## Step 9: Verify - -```zsh -# 1. Syntax check without sourcing -zsh -n ~/.zshrc - -# 2. Run in isolated environment (no user config) -zsh --no-rcs -c 'source ~/.zshrc; echo ok' - -# 3. Profile startup time -time zsh -i -c exit - -# 4. Trace file loading order -zsh --sourcetrace -i -c exit 2>&1 | head - -# 5. Profile compinit specifically -zsh -i -c 'zmodload zsh/zprof; compinit; zprof' 2>&1 | head -20 -``` - -**Glob quoting pitfalls**: With `EXTENDED_GLOB` set, `^`, `~`, and `#` are active in unquoted contexts. Quote them in strings: `echo "it's ~fine"` passes literally; `echo it's ~fine` expands `~fine` as a path. - -**NULL_GLOB vs no-match error**: Without `NULL_GLOB`, `rm *.tmp` fails with "no matches" when no `.tmp` files exist. With `NULL_GLOB`, `rm *.tmp` expands to `rm` with no arguments (also an error). Use glob qualifier `(N)` for conditional expansion: `rm -- *.tmp(N)` silently does nothing when there are no matches. - ---- - -## Error Handling - -### Error: `compinit: insecure directories` - -Cause: Files in `fpath` are group- or world-writable. -Solution: `compaudit` lists the offending paths. Fix with `chmod go-w /path` or pass `-u` flag: `compinit -u` (unsafe — skips ownership check). - -### Error: Completions not found after adding to fpath - -Cause: `compinit` was called before the `fpath` extension, or `~/.zcompdump` is stale. -Solution: Move `fpath=(~/.zsh/completions $fpath)` above `compinit`. Delete `~/.zcompdump` and restart shell to regenerate. - -### Error: `add-zsh-hook: function not found` - -Cause: `autoload -Uz add-zsh-hook` was not called before using the hook. -Solution: Add `autoload -Uz add-zsh-hook` before any `add-zsh-hook` call — typically at the top of `.zshrc`. - -### Error: PATH additions not visible to scripts - -Cause: Variables set in `~/.zshrc` are not sourced by non-interactive shells. -Solution: Move `PATH` exports to `~/.zshenv` — it is sourced by every Zsh invocation including scripts. - -### Error: Slow shell startup (>300ms) - -Cause: `compinit` rescanning all fpath entries on every start; heavy plugin loading. -Solution: Add the `~/.zcompdump` age guard (see Step 3). Profile with `zmodload zsh/zprof; compinit; zprof`. Use `zinit` or lazy-loading for plugins. - -### Error: `zsh -n` reports syntax error in valid-looking code - -Cause: `EXTENDED_GLOB` patterns like `^` or `#` in unquoted strings parsed as glob operators. -Solution: Quote strings containing these characters. Check with `setopt NO_EXTENDED_GLOB` temporarily to isolate. - ---- - -## References - -| Task Signal | Load | Why | -|-------------|------|-----| -| Migrating from Bash, converting `.bashrc`, `export VAR=`, `[[ ]]`, arrays, heredocs | `bash-migration.md` | Full Bash→Zsh syntax translation table | -| Variable scoping, `typeset` flags, `${(f)}` `${(s:,:)}` expansion, `$REPLY`, `$MATCH` | `zsh-quick-reference.md` | Parameter expansion flags, special variables, control flow cheatsheet | -| Error audit, PATH not persisting, completions slow, glob errors, hook not firing | `zsh-preferred-patterns.md` | Failure modes with grep detection commands and error-fix mappings | -| Go, Rust, Docker, Node.js, Python, pyenv, fnm, starship, direnv, fzf, zoxide, mise | `tool-integrations.md` | Concrete integration patterns for common dev tools | - -- `${CLAUDE_SKILL_DIR}/references/bash-migration.md`: Complete Bash-to-Zsh syntax translation table -- `${CLAUDE_SKILL_DIR}/references/zsh-quick-reference.md`: Variable scoping, parameter expansion flags, and special variables -- `${CLAUDE_SKILL_DIR}/references/zsh-preferred-patterns.md`: Failure mode catalog with grep detection commands and error-fix mappings -- `${CLAUDE_SKILL_DIR}/references/tool-integrations.md`: Concrete integration patterns for Go, Rust, Docker, Node.js, Python, and shell enhancers diff --git a/skills/infrastructure/zsh-shell-config/references/bash-migration.md b/skills/infrastructure/zsh-shell-config/references/bash-migration.md deleted file mode 100644 index 892470faa..000000000 --- a/skills/infrastructure/zsh-shell-config/references/bash-migration.md +++ /dev/null @@ -1,248 +0,0 @@ -# Bash to Zsh Migration Reference - -> **Scope**: Bash→Zsh syntax translation for shell config migration. Covers variables, arrays, conditionals, loops, functions, traps, process substitution, heredocs, and parameter expansion. -> **Version range**: Zsh 5.8+ -> **Generated**: 2026-05-26 - ---- - -## Syntax Translation Table - -| Bash | Zsh | Notes | -|------|-----|-------| -| `VAR=value` | `VAR=value` | Both work — Zsh is POSIX-compatible | -| `export VAR=value` | `export VAR=value` or `typeset -x VAR=value` | Both work; `typeset -x` is Zsh-native | -| `unset VAR` | `unset VAR` | Same syntax | -| `$?` | `$?` | Exit status — identical | -| `$@` or `$*` | `$@` or `$*` | Function arguments — identical | -| `$#` | `$#` | Argument count — identical | -| `${var:-default}` | `${var:-default}` | Default value — identical | -| `$(command)` | `$(command)` | Command substitution — identical | -| `[[ condition ]]` | `[[ condition ]]` | Both work — `[[ ]]` is native in Zsh | -| `[ condition ]` | `[ condition ]` or `[[ ]]` | Both work; prefer `[[ ]]` in Zsh | -| `&&` / `\|\|` | `&&` / `\|\|` | Identical | -| `function name() { }` | `function name { }` or `name() { }` | Both work in Zsh | -| `source file` | `source file` or `. file` | Identical | -| `declare -a arr` | `typeset -a arr` | `declare` works but `typeset` is canonical | -| `declare -A assoc` | `typeset -A assoc` | Associative arrays — same semantics | -| `declare -i int` | `typeset -i int` | Integer — same semantics | -| `declare -r CONST` | `typeset -r CONST` | Read-only — same semantics | -| `local var` | `local var` | Both work inside functions | -| `array=(one two three)` | `array=(one two three)` | Same syntax | -| `${array[0]}` | `${array[1]}` | **Zsh arrays are 1-indexed** | -| `${#array[@]}` | `${#array[@]}` or `$#array` | Count — `$#array` is Zsh shorthand | -| `export PATH="$PATH:/new"` | `path+=/new` or `path=(/new $path)` | Use `path` array — Zsh-native | -| `trap 'cmd' SIGTERM` | `trap 'cmd' TERM` | Zsh omits `SIG` prefix | -| `diff <(sort f1) <(sort f2)` | `diff <(sort f1) <(sort f2)` | Process substitution — identical | -| `heredoc < **Scope**: Concrete zsh integration patterns for common dev tools (Go, Rust, Docker, Node.js, Python, and shell enhancers). Covers PATH setup, eval hooks, and alias patterns. Does not cover tool installation. -> **Version range**: Zsh 5.8+; tool-version notes inline where behavior differs -> **Generated**: 2026-05-26 - ---- - -## Overview - -Each tool integration follows the same structure: (1) PATH and env setup in `~/.zshenv`, (2) eval hook with availability guard in `~/.zshrc`, (3) aliases in `~/.zshrc` inside an interactive guard. Use `(( $+commands[tool] ))` (hash lookup, no subprocess) or `command -v tool &>/dev/null` (POSIX, spawns subprocess) for availability checks. - ---- - -## Pattern Table - -| Tool | Init Pattern | PATH Location | Version Notes | -|------|-------------|---------------|---------------| -| Go | `export GOPATH` | `path+=(~/go/bin)` | Go 1.16+: `GOPATH` defaults to `~/go` | -| Rust/Cargo | source `~/.cargo/env` | `path+=(~/.cargo/bin)` | rustup writes `~/.cargo/env` for sh/bash/zsh | -| Node/fnm | `eval "$(fnm env --use-on-cd)"` | handled by fnm | fnm 1.32+: native zsh support | -| Python/pyenv | `eval "$(pyenv init -)"` | `path+=(~/.pyenv/bin ~/.pyenv/shims)` | pyenv 2.0+: use `pyenv init -` | -| Docker | none (PATH handled by installer) | none | aliases only | -| starship | `eval "$(starship init zsh)"` | none | requires starship 0.46+ | -| direnv | `eval "$(direnv hook zsh)"` | none | requires direnv 2.21+ | -| fzf | `source <(fzf --zsh)` | none | `--zsh` flag: fzf 0.48+; older: source `~/.fzf.zsh` | -| zoxide | `eval "$(zoxide init zsh)"` | none | all versions | -| mise | `eval "$(mise activate zsh)"` | none | replaces asdf | -| Homebrew (macOS) | `eval "$(brew shellenv)"` | handled by shellenv | place in `~/.zprofile` for login shells | - ---- - -## Availability Check Patterns - -```zsh -# Preferred: hash lookup (fastest, no subprocess) -if (( $+commands[starship] )); then - eval "$(starship init zsh)" -fi - -# Alternative: command -v (POSIX, spawns subprocess) -if command -v direnv &>/dev/null; then - eval "$(direnv hook zsh)" -fi - -# For checking files/directories (not commands) -if [[ -f ~/.cargo/env ]]; then - source ~/.cargo/env -fi - -if [[ -d ~/.pyenv ]]; then - path=(~/.pyenv/bin ~/.pyenv/shims $path) -fi -``` - ---- - -## Go - -```zsh -# ~/.zshenv -export GOPATH=~/go -# GOROOT only needed for non-standard Go installations: -# export GOROOT=/usr/local/go - -typeset -U path -path=(~/go/bin $path) # Compiled binaries (go install output) -# path=(/usr/local/go/bin $path) # Go toolchain — only if not already in PATH -export PATH -``` - -```zsh -# ~/.zshrc — aliases (inside interactive check or at top level, zsh always checks) -if (( $+commands[go] )); then - alias gob="go build ./..." - alias got="go test ./..." - alias gotr="go test -race ./..." - alias gom="go mod tidy" - alias gor="go run ." - alias goi="go install ./..." -fi -``` - -**Version note**: Go 1.16+ sets `GOPATH` to `~/go` automatically if unset. Explicitly setting it is harmless but documents the intent. `GOROOT` is only needed for non-standard installations (e.g., Go installed via tarball to a custom path). - ---- - -## Rust / Cargo - -```zsh -# ~/.zshenv -# Option A: source rustup's env script (sets PATH, CARGO_HOME, RUSTUP_HOME) -[[ -f ~/.cargo/env ]] && source ~/.cargo/env - -# Option B: direct path addition (simpler, equivalent for most setups) -typeset -U path -path=(~/.cargo/bin $path) -export PATH -``` - -```zsh -# ~/.zshrc — aliases -if (( $+commands[cargo] )); then - alias cb="cargo build" - alias cbr="cargo build --release" - alias ct="cargo test" - alias cr="cargo run" - alias cc="cargo check" - alias ccl="cargo clippy" - alias cf="cargo fmt" - alias cfd="cargo fmt --check" -fi -``` - -**Note**: `rustup` writes `~/.cargo/env` — a sh/bash/zsh-compatible script that sets `PATH`, `CARGO_HOME`, and `RUSTUP_HOME`. Sourcing it is equivalent to manually adding `~/.cargo/bin` to PATH. Both approaches work; sourcing `~/.cargo/env` is more robust when `CARGO_HOME` is customized. - ---- - -## Node.js — fnm (Recommended) - -```zsh -# ~/.zshrc -if (( $+commands[fnm] )); then - eval "$(fnm env --use-on-cd)" # Auto-switches Node version on cd -fi -``` - -```zsh -# ~/.zshrc — aliases -if (( $+commands[fnm] )); then - alias ni="npm install" - alias nid="npm install --save-dev" - alias nr="npm run" - alias nx="npx" - alias nls="fnm list" - alias nuse="fnm use" -fi -``` - -**Version note**: `fnm env --use-on-cd` requires fnm 1.32+. For older fnm, use `eval "$(fnm env)"` without `--use-on-cd` and run `fnm use` manually when changing projects. - -## Node.js — nvm (Alternative) - -```zsh -# ~/.zshrc — nvm is bash-native; source directly in zsh (zsh is sh-compatible) -if [[ -s ~/.nvm/nvm.sh ]]; then - source ~/.nvm/nvm.sh # Load nvm - source ~/.nvm/bash_completion # Optional: nvm tab completion -fi -``` - -**Note**: nvm works in Zsh despite being written for Bash — Zsh's POSIX compatibility handles it. For a native experience, prefer fnm (faster, has native Zsh hooks) or install the `zsh-nvm` plugin. - ---- - -## Python — pyenv - -```zsh -# ~/.zshenv -if [[ -d ~/.pyenv ]]; then - typeset -U path - path=(~/.pyenv/bin ~/.pyenv/shims $path) - export PATH - export PYENV_ROOT=~/.pyenv -fi -``` - -```zsh -# ~/.zshrc -if (( $+commands[pyenv] )); then - eval "$(pyenv init -)" -fi -``` - -```zsh -# ~/.zshrc — aliases -if (( $+commands[pyenv] )); then - alias py="python" - alias pip="python -m pip" -fi -``` - -**Version note**: pyenv 2.0+ uses `pyenv init -` (no language argument). Older pyenv (< 2.0) used the same syntax but without the `--path` / `--no-path` flags added later. Both work; the newer form is preferred. - -## Python — venv Helper - -```zsh -# ~/.zshrc — venv activation function -venv() { - local venv_dir="${1:-.venv}" - if [[ -d "$venv_dir" ]]; then - source "$venv_dir/bin/activate" - else - python -m venv "$venv_dir" && source "$venv_dir/bin/activate" - fi -} -``` - ---- - -## Docker / Docker Compose - -```zsh -# ~/.zshrc — aliases (no init hook needed; Docker modifies PATH at install) -if (( $+commands[docker] )); then - alias d="docker" - alias dc="docker compose" - alias dcu="docker compose up" - alias dcud="docker compose up -d" - alias dcd="docker compose down" - alias dcl="docker compose logs -f" - alias dps="docker ps" - alias dpsa="docker ps -a" - alias dex="docker exec -it" - alias drm="docker rm" - alias drmi="docker rmi" - alias dprune="docker system prune -af" -fi -``` - -**Note**: `docker compose` (V2, no hyphen) is the current syntax. `docker-compose` (V1, with hyphen) is deprecated since Docker Desktop 4.20 / Docker Engine 23.0. - ---- - -## Shell Enhancers - -### starship (Cross-Shell Prompt) - -```zsh -# ~/.zshrc -if (( $+commands[starship] )); then - eval "$(starship init zsh)" -fi -``` - -**Version note**: starship 0.46+ has native Zsh support. Place this after any framework loading (oh-my-zsh, prezto) so starship overrides their prompt. - -### direnv (Per-Directory Env) - -```zsh -# ~/.zshrc -if (( $+commands[direnv] )); then - eval "$(direnv hook zsh)" -fi -``` - -**Note**: direnv 2.21+ has native Zsh hook support. The hook wraps `cd` to load/unload `.envrc` files automatically. Does not slow down non-cd operations. - -### fzf (Fuzzy Finder) - -```zsh -# ~/.zshrc — fzf 0.48+ (native --zsh flag) -if (( $+commands[fzf] )); then - source <(fzf --zsh) -fi - -# Fallback for fzf < 0.48 (installed via package manager) -# if [[ -f ~/.fzf.zsh ]]; then -# source ~/.fzf.zsh -# fi -``` - -**Version note**: `fzf --zsh` (outputting Zsh-native key bindings) was introduced in fzf 0.48.0 (2024-01). For older fzf, the `~/.fzf.zsh` file (written by `fzf --install`) provides equivalent Ctrl+R, Ctrl+T, Alt+C bindings. - -### zoxide (Smart cd Replacement) - -```zsh -# ~/.zshrc -if (( $+commands[zoxide] )); then - eval "$(zoxide init zsh)" - # After init: 'z dirname' jumps to frecent match; 'zi' opens fzf selector -fi -``` - -### mise / rtx (Version Manager, asdf Replacement) - -```zsh -# ~/.zshrc -if (( $+commands[mise] )); then - eval "$(mise activate zsh)" -fi -``` - -**Note**: mise (formerly rtx) replaces asdf for managing language versions (Node, Python, Go, Ruby, etc.). The Zsh activation hook manages shims automatically. Remove asdf integration if switching to mise — they conflict. - -### Homebrew (macOS) - -```zsh -# ~/.zprofile — login shells only (brew shellenv is slow; run once per login) -if [[ -x /opt/homebrew/bin/brew ]]; then - eval "$(/opt/homebrew/bin/brew shellenv)" -fi - -# Intel Mac -if [[ -x /usr/local/bin/brew ]]; then - eval "$(/usr/local/bin/brew shellenv)" -fi -``` - -**Note**: Place in `~/.zprofile` (login shells), not `~/.zshrc` (interactive only). `brew shellenv` sets `HOMEBREW_PREFIX`, `HOMEBREW_CELLAR`, `HOMEBREW_REPOSITORY`, and adds Homebrew bin/sbin to PATH. - ---- - -## Full ~/.zshrc Tool Block Example - -```zsh -# ~/.zshrc — tool integrations block (place near end of file) - -# Version managers (mise replaces asdf) -if (( $+commands[mise] )); then - eval "$(mise activate zsh)" -fi - -# Python -if (( $+commands[pyenv] )); then - eval "$(pyenv init -)" -fi - -# Node -if (( $+commands[fnm] )); then - eval "$(fnm env --use-on-cd)" -fi - -# Prompt -if (( $+commands[starship] )); then - eval "$(starship init zsh)" -fi - -# Directory env -if (( $+commands[direnv] )); then - eval "$(direnv hook zsh)" -fi - -# Smart cd -if (( $+commands[zoxide] )); then - eval "$(zoxide init zsh)" -fi - -# Fuzzy finder -if (( $+commands[fzf] )); then - source <(fzf --zsh) -fi -``` - ---- - -## Error-Fix Mappings - -| Error / Symptom | Root Cause | Fix | -|-----------------|------------|-----| -| `command not found: go` after install | `~/go/bin` not in PATH | Add `path=(~/go/bin $path)` to `~/.zshenv` | -| `cargo: command not found` | `~/.cargo/bin` not in PATH | Add `source ~/.cargo/env` or `path+=(~/.cargo/bin)` to `~/.zshenv` | -| `fnm: Unknown version` on new shell | `--use-on-cd` not in init | Change to `eval "$(fnm env --use-on-cd)"` | -| direnv not loading `.envrc` | direnv hook not sourced | Add `eval "$(direnv hook zsh)"` to `~/.zshrc` | -| `fzf --zsh` flag unknown | fzf version < 0.48 | Use `source ~/.fzf.zsh` instead | -| `pyenv: command not found` | pyenv shims not in PATH | Add `path=(~/.pyenv/bin ~/.pyenv/shims $path)` to `~/.zshenv` | -| Tool init error on every shell | Init without availability guard | Wrap in `(( $+commands[tool] ))` check | -| `GOPATH` not set for scripts | `GOPATH` in `~/.zshrc` not `~/.zshenv` | Move `export GOPATH` to `~/.zshenv` | -| Homebrew tools not found in scripts | `brew shellenv` in `~/.zshrc` | Move `eval "$(brew shellenv)"` to `~/.zprofile` | -| starship prompt missing on non-login shells | `starship init` in `~/.zprofile` | Move to `~/.zshrc` | - ---- - -## See Also - -- `zsh-preferred-patterns.md` — Detection commands for common Zsh config mistakes -- `bash-migration.md` — Bash→Zsh syntax translation -- `zsh-quick-reference.md` — Variable scoping and PATH management cheatsheet diff --git a/skills/infrastructure/zsh-shell-config/references/zsh-preferred-patterns.md b/skills/infrastructure/zsh-shell-config/references/zsh-preferred-patterns.md deleted file mode 100644 index 8416bc142..000000000 --- a/skills/infrastructure/zsh-shell-config/references/zsh-preferred-patterns.md +++ /dev/null @@ -1,289 +0,0 @@ -# Zsh Configuration Patterns - -> **Scope**: Zsh-native patterns for PATH management, variable scoping, completions, and tool integration. Covers correct configuration for Zsh 5.8+ with detection commands for common violations. -> **Version range**: Zsh 5.8+ -> **Generated**: 2026-05-26 - ---- - -## Overview - -Zsh configuration errors rarely produce loud failures — they produce silent wrong behavior: PATH entries that vanish after restart, completions that work once then break, hooks that overwrite each other, or variables invisible to scripts. The patterns below show the Zsh-native approach for each common task, with detection commands to find violations in existing configs. - ---- - -## Pattern Catalog - -### Use `typeset -U path` for PATH Deduplication - -Declare `path` as a unique array at the top of `~/.zshenv`. Every subsequent assignment deduplicates automatically. - -```zsh -# ~/.zshenv — top of file -typeset -U path -path=(~/bin ~/.local/bin $path) -export PATH -``` - -**Why this matters**: Without `typeset -U`, repeated shell starts (tmux panes, subshells) cause PATH to grow with duplicate entries. Commands may unexpectedly resolve to earlier (possibly stale) versions of the same binary. - -**Detection**: -```bash -grep -n 'export PATH' ~/.zshenv ~/.zshrc 2>/dev/null | grep -v 'typeset -U' -grep -n 'PATH.*:.*PATH' ~/.zshenv ~/.zshrc 2>/dev/null -``` - -**Preferred action**: Add `typeset -U path` as the first line in `~/.zshenv`. Replace string-concatenation PATH assignments (`export PATH="$PATH:/new"`) with array-style: `path+=(/new/path)`. - ---- - -### Use `.zshenv` for Environment Variables (Not `.zshrc`) - -Place `PATH`, `EDITOR`, `LANG`, `GOPATH`, and other exported variables in `~/.zshenv`. - -```zsh -# ~/.zshenv — correct -export EDITOR=nvim -export LANG=en_US.UTF-8 -export GOPATH=~/go -typeset -U path -path=(~/go/bin $path) -export PATH -``` - -**Why this matters**: `~/.zshrc` is sourced only for interactive shells. Scripts, cron jobs, and processes spawned by editors (e.g., neovim terminal) source `~/.zshenv` only. Variables in `~/.zshrc` are invisible to those processes. - -**Detection**: -```bash -grep -n '^export\s\+\(EDITOR\|LANG\|GOPATH\|CARGO_HOME\|PATH\)' ~/.zshrc 2>/dev/null -``` - -**Preferred action**: Move exported environment variables from `~/.zshrc` to `~/.zshenv`. Keep aliases, functions, completions, and prompt in `~/.zshrc`. - ---- - -### Use `compinit` Guards to Avoid Slow Startup - -Guard `compinit` with a `~/.zcompdump` age check so it only rescans fpath once per day. - -```zsh -# ~/.zshrc — fast compinit -autoload -Uz compinit -if [[ -n ~/.zcompdump(#qN.mh+24) ]]; then - compinit -else - compinit -C # -C: use cached dump, skip security check -fi -``` - -**Why this matters**: Without the guard, `compinit` scans every file in every fpath directory on every shell start. With 20+ fpath directories (common with oh-my-zsh or homebrew), this adds 200–500ms per shell open. - -**Detection**: -```bash -# Measure startup time -time zsh -i -c exit - -# Profile compinit -zsh -i -c 'zmodload zsh/zprof; compinit; zprof' 2>&1 | head -20 -``` - -**Preferred action**: Add the age guard around `compinit`. Regenerate `~/.zcompdump` manually when adding new completion scripts: `rm ~/.zcompdump && compinit`. - ---- - -### Use `add-zsh-hook` Instead of Overwriting `precmd`/`preexec` - -Attach hooks with `add-zsh-hook` — this preserves existing handlers. - -```zsh -autoload -Uz add-zsh-hook - -_my_precmd() { print -Pn "\e]0;%~\a"; } -add-zsh-hook precmd _my_precmd - -_my_preexec() { print -Pn "\e]0;$1\a"; } -add-zsh-hook preexec _my_preexec -``` - -**Why this matters**: Writing `precmd() { ... }` directly replaces the entire `precmd` function. If a prompt theme or another plugin already defined `precmd`, that definition is lost — causing silent breakage of the overwritten handler (typically the prompt's vcs_info update). - -**Detection**: -```bash -grep -n '^precmd()\|^preexec()\|^chpwd()' ~/.zshrc ~/.zsh/*.zsh 2>/dev/null -``` - -**Preferred action**: Replace bare `precmd() { ... }` function definitions with `add-zsh-hook precmd my_function_name`. Verify `autoload -Uz add-zsh-hook` is called first. - ---- - -### Use Glob Qualifier `(N)` to Prevent "No Matches" Errors - -Append `(N)` to globs that may match nothing when `NULL_GLOB` is not set, or when passing to commands that error on empty args. - -```zsh -# Without (N): error if no .log files exist -rm -- *.log # zsh: no matches found: *.log - -# With (N): silently expands to nothing -rm -- *.log(N) # no error if no matches - -# Practical uses -for f in /var/log/*.log(N); do - gzip "$f" -done - -# Check existence before glob -local files=(~/.zsh/completions/*(N)) -(( ${#files} > 0 )) && echo "Found ${#files} completions" -``` - -**Why this matters**: With `EXTENDED_GLOB` set (common default), unmatched globs throw an error and abort the current script or function. `(N)` is per-glob; `NULL_GLOB` is global (changes behavior for all globs in the session). - -**Detection**: -```bash -grep -n '\*\.' ~/.zshrc ~/.zshenv 2>/dev/null | grep -v '(N)' -``` - -**Preferred action**: Add `(N)` to any glob used in conditional or cleanup contexts (`rm`, `for`, test). Use `setopt NULL_GLOB` only when you want this behavior session-wide. - ---- - -### Guard Tool Integrations With `(( $+commands[tool] ))` - -Check tool availability before running init hooks. Use `(( $+commands[tool] ))` (hash lookup) or `command -v tool &>/dev/null`. - -```zsh -# ~/.zshrc — guarded tool inits -if (( $+commands[starship] )); then - eval "$(starship init zsh)" -fi - -if (( $+commands[direnv] )); then - eval "$(direnv hook zsh)" -fi - -if (( $+commands[zoxide] )); then - eval "$(zoxide init zsh)" -fi - -# Alternative: command -v (POSIX-compatible) -if command -v fnm &>/dev/null; then - eval "$(fnm env --use-on-cd)" -fi -``` - -**Why this matters**: If a tool is not installed, the bare init command fails with "command not found" — producing an error on every new shell. This is especially visible in CI environments, Docker containers, or machines with minimal tool installations. - -**Detection**: -```bash -grep -n 'eval.*init zsh\|eval.*hook zsh\|eval.*activate zsh' ~/.zshrc 2>/dev/null | grep -v 'command\|+commands' -``` - -**Preferred action**: Wrap every `eval "$(tool init zsh)"` in `(( $+commands[tool] )) &&` or an `if` block. The `$+commands` hash is faster than `command -v` (no subprocess). - ---- - -### Place `fpath` Extensions Before `compinit` - -Always extend `fpath` before calling `compinit`. Completions added after `compinit` are not registered until the next shell start. - -```zsh -# Correct order in ~/.zshrc -typeset -U fpath -fpath=(~/.zsh/completions /opt/homebrew/share/zsh/site-functions $fpath) -autoload -Uz compinit -compinit -``` - -**Why this matters**: `compinit` builds the completion index from the current `fpath` snapshot. Extensions added after `compinit` are in `fpath` but not in the index — `_mycommand` completion is never found even though the file is present. - -**Detection**: -```bash -awk '/fpath=|fpath\+=/,/compinit/' ~/.zshrc 2>/dev/null | grep -c 'compinit' -# If fpath lines appear after compinit, output will be > 0 -``` - -**Preferred action**: Move all `fpath` assignments above the `autoload -Uz compinit && compinit` block. When adding Homebrew completions, use the setup block at the top of `.zshrc` before any plugin loading. - ---- - -### Use `autoload -Uz` Consistently - -Always pass `-Uz` flags to `autoload`: `-U` disables alias expansion (prevents accidental override), `-z` forces Zsh-style autoload. - -```zsh -autoload -Uz compinit -autoload -Uz add-zsh-hook -autoload -Uz vcs_info -autoload -Uz my_custom_function -``` - -**Why this matters**: `autoload` without `-U` expands aliases during function definition, which can silently change behavior if you have an alias that shadows a builtin. `-z` forces Zsh-native loading semantics (as opposed to ksh-compatibility mode). - -**Detection**: -```bash -grep -n '^autoload ' ~/.zshrc 2>/dev/null | grep -v '\-U' -``` - -**Preferred action**: Replace all bare `autoload func` with `autoload -Uz func`. - ---- - -## Error-Fix Mappings - -| Error / Symptom | Root Cause | Fix | -|-----------------|------------|-----| -| PATH grows on each new shell/tmux pane | No `typeset -U path` | Add `typeset -U path` at top of `~/.zshenv` | -| Env var not visible in scripts | Set in `~/.zshrc` | Move to `~/.zshenv` | -| Shell startup > 300ms | `compinit` running without cache guard | Add `~/.zcompdump` age check around `compinit -C` | -| Prompt stops updating after adding `precmd` | Overwrote existing `precmd` | Use `add-zsh-hook precmd my_func` | -| `no matches found: *.ext` error | Glob with no matches, no `(N)` qualifier | Append `(N)`: `*.ext(N)` | -| Tool init error on every shell open | `eval "$(tool init zsh)"` without availability guard | Wrap in `(( $+commands[tool] ))` check | -| Completions not found after adding `_mycommand` | `fpath` extended after `compinit` | Move `fpath` extension above `compinit` | -| `autoload` function uses wrong alias | Missing `-U` flag | Use `autoload -Uz func` | -| `compinit: insecure directories` | World/group-writable fpath entries | `compaudit`; `chmod go-w /path`; or `compinit -u` | -| `add-zsh-hook: command not found` | `autoload -Uz add-zsh-hook` not called | Add `autoload -Uz add-zsh-hook` before hook calls | -| `~/.zcompdump` outdated | Stale cache after adding new completions | `rm ~/.zcompdump && compinit` | - ---- - -## Detection Commands Reference - -```bash -# PATH growing (no typeset -U) -grep -n 'export PATH\|PATH.*:.*PATH' ~/.zshenv ~/.zshrc 2>/dev/null - -# Env vars in wrong file (should be in .zshenv) -grep -n '^export\s\+\(EDITOR\|LANG\|GOPATH\|PATH\)' ~/.zshrc 2>/dev/null - -# Missing compinit cache guard -grep -n 'compinit' ~/.zshrc 2>/dev/null | grep -v '\-C\|zcompdump' - -# Direct precmd/preexec overwrite (should use add-zsh-hook) -grep -n '^precmd()\|^preexec()\|^chpwd()' ~/.zshrc 2>/dev/null - -# Globs without (N) qualifier in loops/cleanup -grep -n 'rm.*\*\.' ~/.zshrc 2>/dev/null | grep -v '(N)' - -# Tool inits without availability guard -grep -n 'eval.*init zsh\|eval.*hook zsh' ~/.zshrc 2>/dev/null | grep -v 'commands\|command -v' - -# fpath after compinit -grep -n 'fpath\|compinit' ~/.zshrc 2>/dev/null - -# autoload without -U flag -grep -n '^autoload ' ~/.zshrc 2>/dev/null | grep -v '\-U' - -# Syntax check all rc files -for f in ~/.zshenv ~/.zprofile ~/.zshrc; do - [[ -f "$f" ]] && zsh -n "$f" || echo "SYNTAX ERROR: $f" -done -``` - ---- - -## See Also - -- `bash-migration.md` — Bash→Zsh syntax translation table -- `zsh-quick-reference.md` — Variable scoping, parameter expansion flags, special variables -- `tool-integrations.md` — Complete tool integration patterns (Go, Rust, Docker, Node.js, pyenv) diff --git a/skills/infrastructure/zsh-shell-config/references/zsh-quick-reference.md b/skills/infrastructure/zsh-shell-config/references/zsh-quick-reference.md deleted file mode 100644 index 7ce4e9ed9..000000000 --- a/skills/infrastructure/zsh-shell-config/references/zsh-quick-reference.md +++ /dev/null @@ -1,332 +0,0 @@ -# Zsh Quick Reference - -> **Scope**: Variable scoping, parameter expansion flags, special variables, fpath management, and control flow. Covers Zsh-specific features not in POSIX sh or Bash. -> **Version range**: Zsh 5.8+ -> **Generated**: 2026-05-26 - ---- - -## Variable Scoping - -### Scope Modifiers - -```zsh -local var="value" # Local to current function (preferred for readability) -typeset var="value" # Same as local inside functions; global at top level -typeset -g var="value" # Force global even inside function -typeset -x var="value" # Export (same as export) -typeset -r var="value" # Read-only -typeset -i var=0 # Integer (arithmetic auto-applied) -typeset -l var="VALUE" # Always stored lowercase -typeset -u var="value" # Always stored uppercase -typeset -a arr # Indexed array -typeset -A assoc # Associative array -typeset -U arr # Unique elements (dedup on assignment) -``` - -### Scope Guidelines - -| Scope | Modifier | Use For | -|-------|----------|---------| -| Function-local | `local` / `typeset` | Loop variables, temporary values in functions | -| Global | `typeset -g` or top-level | Session-wide settings | -| Exported | `typeset -x` / `export` | Variables visible to child processes | -| Read-only | `typeset -r` | Constants | -| Unique array | `typeset -U` | PATH, fpath — auto-deduplicates | - -### Scope Examples - -```zsh -# Function-local variable -my_function() { - local result="" - typeset -i count=0 - # $result and $count are destroyed on function return -} - -# Global exported variable -export EDITOR=nvim -# or equivalently: -typeset -x EDITOR=nvim - -# PATH deduplication (set once at top of .zshenv) -typeset -U path -path=(~/bin ~/.local/bin $path) - -# Integer arithmetic -typeset -i total=0 -total+=5 # total is now 5 (not "05") -``` - ---- - -## Parameter Expansion Flags - -Zsh parameter expansion flags go inside `${(flags)var}` — load them lazily; full list in `man zshexpn`. - -### String Operations - -```zsh -${(U)var} # Uppercase all characters -${(L)var} # Lowercase all characters -${(C)var} # Capitalize first letter of each word -${(q)var} # Shell-quote the value (safe for eval) -${(Q)var} # Remove one level of quoting -${(l:N::c:)var} # Left-pad to width N with char c -${(r:N::c:)var} # Right-pad to width N with char c -${#var} # Length of string (or array element count) -``` - -### Splitting and Joining - -```zsh -${(f)var} # Split on newlines → array -${(s:,:)var} # Split on comma → array (any delimiter in :delim:) -${(z)var} # Split as shell words (respecting quotes) -${(j: :)array} # Join array with space separator -${(j:\n:)array} # Join array with newlines -${(F)array} # Join array with newlines (shorthand for ${(j:\n:)}) -``` - -### Array Operations - -```zsh -${(u)array} # Unique elements (deduplicated) -${(o)array} # Sort ascending -${(O)array} # Sort descending -${(i)array} # Sort case-insensitively -${(n)array} # Sort numerically -${(r)array} # Reverse order -${(k)assoc} # Keys of associative array -${(v)assoc} # Values of associative array -${(kv)assoc} # Interleaved key-value pairs -``` - -### Practical Examples - -```zsh -# Split a colon-separated PATH into array -path_parts=("${(s.:.)PATH}") - -# Split command output into lines array -lines=("${(f)$(cat file.txt)}") - -# Join array back to string -joined="${(j:,:)myarray}" - -# Get unique items from array -typeset -U myarray -# or without typeset: -unique=("${(u)myarray[@]}") - -# Lowercase a string -lower="${(L)MY_UPPER_VAR}" - -# Shell-safe quoting for dynamic eval -safe="${(q)user_input}" - -# Split on newlines and iterate -while IFS= read -r line; do - # ... -done <<< "${(F)myarray}" -``` - ---- - -## fpath Management - -`fpath` is to functions what `PATH` is to executables. Zsh searches `fpath` for autoloaded functions and completion scripts. - -```zsh -# ~/.zshrc — extend fpath before compinit - -# Dedup fpath (same as path) -typeset -U fpath - -# Add custom completion directory -fpath=(~/.zsh/completions $fpath) - -# Add custom functions directory -fpath=(~/.zsh/functions $fpath) - -# Homebrew completions (macOS) -if [[ -d /opt/homebrew/share/zsh/site-functions ]]; then - fpath=(/opt/homebrew/share/zsh/site-functions $fpath) -fi - -# Autoload a function from fpath (lazy — loads on first call) -autoload -Uz my_function - -# Autoload all functions in a directory -for f in ~/.zsh/functions/*; do - autoload -Uz "${f:t}" # :t = tail (basename) -done -``` - -**Completion script naming**: Completion functions must be named `_command` (underscore prefix). Place `_mycommand` in a `fpath` directory, then `compinit` finds it automatically — no explicit `autoload` needed. - ---- - -## Special Variables - -```zsh -$? # Exit status of last command -$! # PID of last background process -$$ # PID of current shell -$0 # Name of current script or shell -$SHELL # Path to current shell binary -$ZSH_VERSION # Zsh version string (e.g., "5.9") -$SHLVL # Shell nesting level (increments per subshell) - -# History -$HISTFILE # Path to history file (default: ~/.zsh_history) -$HISTSIZE # Max commands in memory -$SAVEHIST # Max commands saved to HISTFILE - -# Prompt -$PROMPT # Primary prompt (same as $PS1) -$PS1 # Primary prompt (POSIX compatible) -$RPROMPT # Right-side prompt (Zsh-only) -$PS2 # Continuation prompt (default: "> ") -$PS3 # Select menu prompt -$PS4 # Xtrace prefix (default: "+ ") - -# Completion/match state -$REPLY # Default variable for read builtin; also set by some builtins -$reply # Array version of $REPLY (lowercase) -$MATCH # String matched by last regex (with =~ or pcre_match) -$match # Array of capture groups from last regex match -$MBEGIN # Start position of last regex match -$MEND # End position of last regex match -$compstate # Associative array of completion state (in completion functions) - -# Directory -$PWD # Current directory -$OLDPWD # Previous directory (used by cd -) -$CDPATH # Colon-separated list of dirs to search for cd targets - -# Zsh-specific -$ZSH_ARGZERO # $0 equivalent but not modified by function calls -$LINENO # Current line number in script -$COLUMNS # Terminal width -$LINES # Terminal height -``` - ---- - -## Control Flow Cheatsheet - -```zsh -# If/elif/else -if [[ condition ]]; then - action -elif [[ other ]]; then - other_action -else - fallback -fi - -# Common conditions -[[ -f file ]] # File exists and is regular file -[[ -d dir ]] # Directory exists -[[ -e path ]] # Path exists (any type) -[[ -r file ]] # Readable -[[ -w file ]] # Writable -[[ -x file ]] # Executable -[[ -s file ]] # Non-empty file -[[ -L link ]] # Symbolic link -[[ -z "$var" ]] # String is empty -[[ -n "$var" ]] # String is not empty -[[ "$a" == "$b" ]] # String equality (glob patterns on right side) -[[ "$a" =~ regex ]] # Regex match (POSIX ERE; match in $MATCH) -[[ $n -eq 5 ]] # Numeric equal -[[ $n -lt 5 ]] # Numeric less than -[[ $n -gt 5 ]] # Numeric greater than - -# Case -case "$var" in - start|begin) action ;; - stop|end) other_action ;; - *) default_action ;; -esac - -# For loop -for item in one two three; do - echo "$item" -done - -# For loop over array -for item in "${array[@]}"; do - echo "$item" -done - -# C-style for loop -for (( i=0; i<10; i++ )); do - echo "$i" -done - -# While loop -while [[ condition ]]; do - action -done - -# Read lines from file -while IFS= read -r line; do - echo "$line" -done < file.txt - -# Until loop -until [[ condition ]]; do - action -done - -# Select menu -select choice in option1 option2 quit; do - case "$choice" in - option1) handle1 ;; - option2) handle2 ;; - quit) break ;; - esac -done - -# Arithmetic -(( total = a + b )) -(( i++ )) -result=$(( 2 ** 8 )) # 256 -``` - ---- - -## Debugging and Inspection - -```zsh -# Syntax check without executing -zsh -n script.zsh - -# Trace execution -set -x # Enable xtrace (echoes each command) -set +x # Disable xtrace - -# Check variable value and type -typeset -p var # Print variable with flags and value -print -l "${path[@]}" # Print array one per line - -# Check if variable is set -[[ -v var ]] # True if var is set (any value including empty) -[[ -n "${var+x}" ]] # POSIX-compatible set check - -# Profile startup -zsh -i -c 'zmodload zsh/zprof; source ~/.zshrc; zprof' - -# Check which file defines a function -which function_name # or: whence -v function_name -type function_name # Full type info -``` - ---- - -## See Also - -- `bash-migration.md` — Bash→Zsh syntax translation table -- `zsh-preferred-patterns.md` — Failure modes with detection commands -- `tool-integrations.md` — Tool integration patterns diff --git a/skills/meta/do/SKILL.md b/skills/meta/do/SKILL.md index dc7d380e3..871bbb752 100644 --- a/skills/meta/do/SKILL.md +++ b/skills/meta/do/SKILL.md @@ -98,8 +98,8 @@ No fit→null→general-purpose. Never skill→agent. FORCE skills: skill slot o FORCE-ROUTE — select when domain matches SEMANTICALLY (meaning, not words): - "push my changes" → pr-workflow (FORCE) ✓ (git push) - "push back on this design" → NOT pr-workflow (resist/argue) -- "configure my fish shell" → fish-shell-config (FORCE) ✓ -- "fish for bugs" → NOT fish-shell-config (search for bugs) +- "configure my fish shell" → shell-config (FORCE) ✓ +- "fish for bugs" → NOT shell-config (search for bugs) - "quick fix to the login page" → quick (FORCE) ✓ - "quick overview of the architecture" → NOT quick (exploration) diff --git a/skills/meta/skill-creator/SKILL.md b/skills/meta/skill-creator/SKILL.md index 283785e63..e919b1b03 100644 --- a/skills/meta/skill-creator/SKILL.md +++ b/skills/meta/skill-creator/SKILL.md @@ -198,7 +198,7 @@ Set `force_route: true` when the skill matches one of these patterns: | Pattern | Examples | |---------|----------| | Umbrella / lifecycle (one trigger phrase = one phase) | `planning`, `feature-lifecycle`, `pr-workflow` | -| Setup or installation (user states explicit intent) | `install`, `fish-shell-config` | +| Setup or installation (user states explicit intent) | `install`, `shell-config` | | Framework scaffolding (no semantic ambiguity) | `agent-creator` | | Deterministic methodology tools (user names the tool) | `quick`, `python-quality-gate`, `go-patterns` | | Trace/diagnostic queries (verb + noun is unambiguous) | `explanation-traces` | diff --git a/skills/meta/skill-creator/references/domain-research-targets.md b/skills/meta/skill-creator/references/domain-research-targets.md index 8c09820f1..c41987c3e 100644 --- a/skills/meta/skill-creator/references/domain-research-targets.md +++ b/skills/meta/skill-creator/references/domain-research-targets.md @@ -88,7 +88,7 @@ existing sapcc references --- -## Kubernetes (kubernetes-debugging, kubernetes-security) +## Kubernetes (kubernetes umbrella: debugging + security references) **Primary sources** - [Kubernetes official docs](https://kubernetes.io/docs/) — especially Concepts and