# phxagents — full documentation > Canonical phxagents skills, agents, runtime guides, hook documentation, the origin essay, and research. Per-skill reference appendices are intentionally excluded. ## ash-framework # Ash Framework Reference Reference for Ash Framework in Phoenix/LiveView projects. Ash complements Phoenix/Ecto — LiveView, security, and OTP Iron Laws still apply. Only data access patterns shift toward Ash actions and domain code interfaces. ## Iron Laws 1. **USE DOMAIN CODE INTERFACES** — Never call `Ash.create/Ash.read` directly in LiveViews or Controllers; use domain code interfaces: `MyApp.Accounts.register_user()` not `Ash.create(User, attrs)` 2. **SET ACTOR/SCOPE AT QUERY PREP, NOT EXECUTION** — Give `actor:` or `scope:` to `for_read/for_create/for_action` (prep), NOT to `Ash.read!/Ash.create!` (execution); execution-level actor bypasses row-level policy evaluation. If project uses `Ash.Scope`, use `scope:` consistently instead of bare `actor:` — do not mix styles 3. **GENERATORS FIRST** — Before writing Ash code manually, run `mix ash.gen.resource` or `mix ash.gen.domain` with `--yes`; check `mix help ash.gen.` for options 4. **CODEGEN AFTER RESOURCE CHANGES** — Always run `mix ash.codegen` after modifying resources; this generates migrations from resource snapshots — never write AshPostgres migrations by hand 5. **ACTIONS OVER FUNCTIONS** — Put business logic in named actions, not domain functions; expose via code interfaces defined on the domain 6. **NEVER EDIT RESOURCE SNAPSHOTS** — `priv/resource_snapshots/` is owned exclusively by `mix ash.codegen`; manual edits corrupt migration tracking 7. **NO DIRECT `Repo.*` IN ASH PROJECTS** — `Repo.all/get/insert` bypass Ash policies and notifications; use domain code interfaces. Any `Repo` call in an Ash project is an escape hatch and must be documented ## Quick Reference ### Domain Code Interface Pattern ```elixir # Domain definition defmodule MyApp.Accounts do use Ash.Domain resources do resource MyApp.Accounts.User do define :register_user, action: :create, args: [:email, :name] define :get_user_by_email, action: :read, get_by: [:email] end end end # In LiveView/Controller — always via domain, never Ash.create directly {:ok, user} = MyApp.Accounts.register_user(email, name, actor: nil) user = MyApp.Accounts.get_user_by_email!(email, actor: current_user) ``` ### Authorization — Actor/Scope at Query Prep ```elixir # CORRECT — actor at query prep, policies evaluated per-row MyApp.Post |> Ash.Query.for_read(:list_published, %{}, actor: current_user) |> Ash.read!() # CORRECT with Ash.Scope (carries actor + tenant + context; use if project adopts it) MyApp.Post |> Ash.Query.for_read(:list_published, %{}, scope: scope) |> Ash.read!() # WRONG — actor at execution bypasses row-level policy evaluation MyApp.Post |> Ash.Query.for_read(:list_published) |> Ash.read!(actor: current_user) ``` ### Ash.Scope — When the Project Uses It `Ash.Scope` bundles `actor + tenant + context` into a single struct carried through actions. Implement `Ash.Scope.ToOpts` on a project-defined scope struct: ```elixir defimpl Ash.Scope.ToOpts, for: MyApp.Scope do def get_actor(%{current_user: u}), do: {:ok, u} def get_tenant(%{current_tenant: t}), do: {:ok, t} def get_context(%{locale: l}), do: {:ok, %{shared: %{locale: l}}} def get_tracer(_), do: :error def get_authorize?(_), do: :error end ``` **Detection**: if the project has a `Scope` module implementing `Ash.Scope.ToOpts`, use `scope:` everywhere instead of bare `actor:`. Do NOT mix the two styles in the same codebase. See `mix usage_rules.docs Ash.Scope` for full protocol spec. ### File Conventions (from `mix ash.gen.*`) | File | Location | Behaviour | | -------------- | --------------------------------- | ----------------------------- | | Changes | `lib/app/ctx/changes/name.ex` | `use Ash.Resource.Change` | | Policy Checks | `lib/app/ctx/checks/name.ex` | `use Ash.Policy.Check` | | Custom Actions | `lib/app/ctx/actions/name.ex` | generic action logic | | Custom Types | `lib/app/ctx/types/name.ex` | `use Ash.Type` | | Validations | `lib/app/ctx/validations/name.ex` | `use Ash.Resource.Validation` | ### Generator Workflow ```bash mix ash.gen.resource MyApp.Accounts.User --yes mix ash.gen.domain MyApp.Accounts --yes mix ash.codegen # reads resource snapshots → generates migration mix ash.migrate ``` ## Research Prefer the highest-fidelity source available: 1. **Tidewave** (exact version from `mix.lock`): ``` mcp__tidewave__get_docs(module: "Ash.Resource") mcp__tidewave__get_docs(module: "AshPhoenix.Form") ``` 2. **usage_rules** (project-synced to your installed ash_* dep versions): ```bash mix usage_rules.search_docs "" -p ash -p ash_phoenix -p ash_postgres -p ash_authentication -p ash_oban mix usage_rules.docs Ash.Resource ``` 3. **WebFetch hexdocs.pm** (fallback when neither is available): ``` WebFetch(url: "https://hexdocs.pm/ash/Ash.Resource.html", prompt: "Extract module docs.") ``` If `usage_rules` is not configured, the SessionStart hook suggests how to install it. --- ## compound-docs # Compound Docs — Institutional Knowledge Base Searchable, categorized solution documentation that makes each debugging session easier than the last. ## Directory Structure ``` .claude/solutions/ ├── ecto-issues/ ├── liveview-issues/ ├── oban-issues/ ├── otp-issues/ ├── security-issues/ ├── testing-issues/ ├── phoenix-issues/ ├── deployment-issues/ ├── performance-issues/ └── build-issues/ ``` ## Iron Laws 1. **ALWAYS search solutions before investigating** — Check `.claude/solutions/` for existing fixes before debugging 2. **YAML frontmatter is MANDATORY** — Every solution needs validated metadata per `${CLAUDE_SKILL_DIR}/references/schema.md` 3. **One problem per file** — Never combine multiple solutions 4. **Include prevention** — Every solution documents how to prevent recurrence ## Solution File Format ```markdown --- module: "Accounts" date: "2025-12-01" problem_type: runtime_error component: ecto_schema symptoms: - "Ecto.Association.NotLoaded on user.posts" root_cause: missing_preload severity: medium tags: [preload, association, n-plus-one] --- # Association NotLoaded on User Posts ## Symptoms Ecto.Association.NotLoaded raised when accessing user.posts in UserListLive after filtering. ## Root Cause Query in Accounts context missing preload for :posts. ## Solution Added `Repo.preload(:posts)` to `list_users/1`. ## Prevention Use n1-check skill before shipping list views. ``` ## Searching Solutions Use Grep to search `.claude/solutions/` by symptom (e.g., `NotLoaded`), by tag (e.g., `tags:.*preload`), or by component (e.g., `component: ecto`). ## Integration - `/phx:compound` creates solution docs here - `/phx:investigate` searches here before debugging - `/phx:plan` consults for known risks - `learn-from-fix` feeds into this system ## References - `${CLAUDE_SKILL_DIR}/references/schema.md` — YAML frontmatter validation schema - `${CLAUDE_SKILL_DIR}/references/resolution-template.md` — Full solution template --- ## deploy # Elixir/Phoenix Deployment Reference Quick reference for deploying Elixir/Phoenix applications. ## Iron Laws — Never Violate These 1. **Config at runtime, not compile time** — Secrets in `config.exs` get baked into the release binary. Use `runtime.exs` with env vars so secrets are resolved at boot 2. **Graceful shutdown ≥ 60 seconds** — Shorter timeouts kill in-flight requests and WebSocket connections mid-operation, causing data loss for users 3. **Health checks required** — Without startup/liveness/readiness endpoints, orchestrators can't distinguish a booting node from a dead one, leading to cascading restarts 4. **SSL verification for database** — Skipping `verify: :verify_peer` allows MITM attacks between your app and database; production data traverses the connection 5. **No CPU limits** — The BEAM scheduler assumes it owns all cores; cgroups CPU limits cause scheduler collapse where the VM thinks it has more cores than it can use, leading to latency spikes 6. **Guard optional service credentials** — `runtime.exs` runs whenever a release boots, including `eval`-based migration commands. Only require S3, Redis, and similar credentials when that integration is enabled ## Quick Configuration ### runtime.exs (Essential) ```elixir if config_env() == :prod do database_url = System.get_env("DATABASE_URL") || raise "DATABASE_URL is required" secret_key_base = System.get_env("SECRET_KEY_BASE") || raise "SECRET_KEY_BASE is required" host = System.get_env("PHX_HOST") || raise "PHX_HOST is required" config :my_app, MyApp.Repo, url: database_url, pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), ssl: true, ssl_opts: [verify: :verify_peer] config :my_app, MyAppWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")], secret_key_base: secret_key_base, server: true end ``` ### Guard Optional Services Keep core boot secrets such as `DATABASE_URL` and `SECRET_KEY_BASE` required. Gate credentials for optional integrations behind the same feature switch that enables the integration: ```elixir s3_config = if System.get_env("STORAGE_BACKEND") == "s3" do [ access_key_id: System.get_env("S3_ACCESS_KEY") || raise("S3_ACCESS_KEY is required when STORAGE_BACKEND=s3"), secret_access_key: System.get_env("S3_SECRET_KEY") || raise("S3_SECRET_KEY is required when STORAGE_BACKEND=s3") ] else [] end config :my_app, :s3_config, s3_config ``` This lets release tasks that do not use S3 start without S3 credentials while still failing fast when S3 is selected. ### Health Check Plug ```elixir def call(%{path_info: ["health", "readiness"]} = conn, _opts) do case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do {:ok, _} -> send_resp(conn, 200, ~s({"status":"ok"})) |> halt() {:error, _} -> send_resp(conn, 503, ~s({"status":"error"})) |> halt() end end ``` ## Quick Decisions ### Platform Choice | Need | Use | |------|-----| | Simple, managed | Fly.io | | Enterprise, existing K8s | Kubernetes | | Custom infrastructure | Docker + your orchestrator | ### Resource Limits | Resource | Recommendation | |----------|----------------| | CPU | **NO LIMITS** (BEAM scheduler issues) | | Memory | Set limits (256Mi-512Mi typical) | | Graceful shutdown | ≥ 60 seconds | ## Deployment Checklist - [ ] All secrets from environment variables in runtime.exs - [ ] Optional service credentials required only when their integration is enabled - [ ] `server: true` in endpoint config - [ ] SSL verification for database connections - [ ] Health endpoints: /health/startup, /health/liveness, /health/readiness - [ ] Graceful shutdown period ≥ 60 seconds - [ ] No CPU limits (memory limits only) - [ ] Migrations in deploy process ## Asset Pipeline Notes Phoenix 1.8 uses esbuild + tailwind (no Node.js required): - Config in `config/config.exs` under `:esbuild` and `:tailwind` - `mix assets.deploy` builds for production - `mix assets.setup` installs binaries on first run - Custom JS bundlers: configure in `config/config.exs` ## References For detailed patterns, see: - `${CLAUDE_SKILL_DIR}/references/docker-config.md` - Multi-stage Dockerfile, best practices - `${CLAUDE_SKILL_DIR}/references/flyio-config.md` - fly.toml, clustering, commands --- ## ecto-patterns # Ecto Patterns Reference Reference for working with Ecto schemas, queries, and migrations. ## Iron Laws — Never Violate These 1. **CHANGESETS ARE FOR EXTERNAL DATA** — Use `cast/4` for user/API input, `change/2` or `put_change/3` for internal trusted data 2. **NEVER USE `:float` FOR MONEY** — Always use `:decimal` or `:integer` (cents) 3. **NO RAILS-STYLE POLYMORPHIC ASSOCIATIONS** — They break foreign key constraints; use multiple nullable FKs or separate join tables 4. **ALWAYS PIN VALUES IN QUERIES** — `u.name == ^user_input` is safe, string interpolation causes SQL injection 5. **PRELOAD COLLECTIONS, NOT INDIVIDUALS** — Preloading in loops = N+1 queries 6. **CONSTRAINTS BEAT VALIDATIONS FOR RACE CONDITIONS** — Validations provide quick feedback, constraints provide DB-level safety 7. **SEPARATE QUERIES FOR `has_many`, JOIN FOR `belongs_to`** — Avoids row multiplication 8. **NO IMPLICIT CROSS JOINS** — `from(a in A, b in B)` without `on:` creates Cartesian product 9. **DEDUP BEFORE `cast_assoc` WITH SHARED DATA** — When multiple parents share child data, deduplicate child records BEFORE building changesets. Dedup only works within a single changeset ## Quick Schema Template ```elixir defmodule MyApp.Context.Entity do use Ecto.Schema import Ecto.Changeset @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id schema "entities" do field :name, :string field :status, Ecto.Enum, values: [:draft, :active, :archived] field :amount_cents, :integer # Never :float for money! belongs_to :user, MyApp.Accounts.User timestamps(type: :utc_datetime_usec) end def changeset(entity, attrs) do entity |> cast(attrs, [:name, :status, :amount_cents]) |> validate_required([:name]) |> foreign_key_constraint(:user_id) end end ``` ## Quick Decisions ### cast vs put_change vs change | Function | Use When | |----------|----------| | `cast/4` | External data (user input, API) | | `put_change/3` | Internal trusted data (timestamps, computed) | | `change/2` | Internal data from existing struct | ### Preload Strategy | Relationship | Strategy | |--------------|----------| | `belongs_to` | JOIN (single query) | | `has_many` | Separate queries (avoid row multiplication) | ## Common Anti-patterns | Wrong | Right | |-------|-------| | `field :amount, :float` | `field :amount_cents, :integer` | | `"SELECT * WHERE name = '#{name}'"` | `from(u in User, where: u.name == ^name)` | | `Repo.all(User) \|> Enum.filter(& &1.active)` | `from(u in User, where: u.active)` | | Preloading in loops | `Repo.preload(posts, :comments)` | | `Repo.get!(User, user_id)` with user input | `Repo.get(User, id)` + handle nil | | `{:ok, _} = Repo.update(cs)` inside `Repo.transaction` | `case`/`with` + `Repo.rollback(cs)` (see transactions.md) | ## References For detailed patterns, see: - `${CLAUDE_SKILL_DIR}/references/changesets.md` - cast vs put_change, custom validations, prepare_changes - `${CLAUDE_SKILL_DIR}/references/queries.md` - Composable queries, dynamic, subqueries, preloading - `${CLAUDE_SKILL_DIR}/references/migrations.md` - Safe migrations, concurrent indexes, NOT NULL - `${CLAUDE_SKILL_DIR}/references/transactions.md` - Repo.transact, Ecto.Multi, upserts --- ## ecto:constraint-debug # Ecto Constraint Debugging > **Ash projects**: Ash surfaces DB constraints through its own error DSL. Use the `ash-framework` skill — `mix usage_rules.search_docs "constraint" -p ash_postgres`. Systematic approach to diagnosing constraint violations. Load when you see `Ecto.ConstraintError`, `unique_constraint`, `foreign_key_constraint`, or constraint-related changeset errors. ## Iron Laws 1. **READ THE CONSTRAINT NAME** — The constraint name (e.g., `links_url_index`) tells you exactly which index/constraint failed. Parse it from the error message first 2. **CHECK MIGRATION BEFORE CODE** — Verify the constraint definition in `priv/repo/migrations/` matches what the schema expects 3. **TRACE ALL INSERT PATHS** — Find every code path that inserts into the constrained table. The bug is often in a path you didn't consider 4. **RACE CONDITION UNTIL PROVEN OTHERWISE** — If validation passes but constraint fails, assume concurrent inserts until you prove a single-request cause ## Step-by-Step Debugging ### Step 1: Parse the Error Extract from the error message: - **Constraint name** (e.g., `users_email_index`) - **Table name** (e.g., `users`) - **Operation** (insert, update, or delete) - **Conflicting values** (if available in logs) ### Step 2: Find the Migration Use Grep to search for the constraint name in `priv/repo/migrations/`. Also check for `create unique_index`, `create index`, `add constraint`. Verify: Does the migration constraint match the schema's `unique_constraint/3` or `foreign_key_constraint/3` call? ### Step 3: Find the Schema Use Grep to find constraint handling in changesets (`unique_constraint`, `foreign_key_constraint`, `check_constraint`) in `lib/`. ### Step 4: Trace Insert Paths Find ALL callers that insert/update this schema: Use Grep to find all insert/update paths (`Repo.insert`, `Repo.update`, `Repo.insert_all`, `cast_assoc`) in `lib/`. ### Step 5: Identify the Cause | Symptom | Likely Cause | Fix Pattern | |---------|-------------|-------------| | Same user triggers twice | Race condition (double-click, retry) | Upsert with `on_conflict` | | Multiple parents share child | `cast_assoc` doesn't dedup across changesets | Dedup before building changesets | | Concurrent API requests | Missing transaction isolation | Wrap in `Repo.transaction` or use upsert | | Migration added constraint to existing data | Data violates new constraint | Backfill or clean data first | ### Step 6: Apply Fix See `${CLAUDE_SKILL_DIR}/references/constraint-patterns.md` for detailed fix patterns. ## Quick Fixes by Constraint Type **Unique violation** → Upsert: `Repo.insert(changeset, on_conflict: :replace_all, conflict_target: [:field])` **Foreign key violation** → Check: Does the referenced record exist? Was it deleted concurrently? **Check constraint** → Validate: Does the value satisfy the constraint condition? ## References - `${CLAUDE_SKILL_DIR}/references/constraint-patterns.md` - Detailed patterns for each constraint type --- ## ecto:n1-check # N+1 Query Detection Identify and fix N+1 query anti-patterns in Ecto/Phoenix applications. ## Iron Laws - Never Violate These 1. **Never access associations without preload** - Always preload before `Enum.map` 2. **No Repo calls inside loops** - Restructure to batch queries 3. **Preload at context boundary** - Load associations in context, not controllers/views 4. **Use joins for filtering** - Use `join` + `preload` when filtering by association ## Detection Patterns ### Pattern 1: Enum.map with Repo ```elixir # BAD: N+1 queries users |> Enum.map(fn user -> Repo.get(Order, user.order_id) end) # GOOD: Single query with preload users |> Repo.preload(:orders) ``` ### Pattern 2: Association Access Without Preload ```elixir # BAD: Lazy loading triggers N queries for user <- users do user.posts # Triggers query for each user! end # GOOD: Eager load first users = Repo.all(User) |> Repo.preload(:posts) for user <- users do user.posts # Already loaded end ``` ### Pattern 3: Nested Association Access ```elixir # BAD: N+1 for nested associations user.posts |> Enum.map(fn post -> post.comments end) # GOOD: Nested preload Repo.preload(user, posts: :comments) ``` ## Quick Detection Commands Use Grep with context lines (`-B 5 -A 5`) to find `Enum.map` near `Repo.` calls in `lib/**/*.ex`. Use Grep to find association access patterns (`.posts`, `.comments`, `.orders`) in `lib/**/*.ex`. Use Grep with context (`-B 3`) to find `Repo.get` or `Repo.one` near loop patterns (`for`, `Enum`) in `lib/**/*.ex`. ## Analysis Command Use Grep to find all `Repo.` calls in a context module, then verify each query has appropriate preloads. ## References For detailed patterns, see: - `${CLAUDE_SKILL_DIR}/references/preload-patterns.md` - Efficient preloading strategies - `${CLAUDE_SKILL_DIR}/references/query-optimization.md` - Query batching techniques --- ## elixir-idioms # Elixir Idioms Reference for writing idiomatic Elixir code with BEAM-aware patterns. ## Iron Laws — Never Violate These 1. **NO PROCESS WITHOUT A RUNTIME REASON** — Processes model concurrency, state, isolation—NOT code structure 2. **MESSAGES ARE COPIED** — Keep messages small (except binaries >64 bytes) 3. **GUARDS USE `and`/`or`/`not`** — Never use short-circuit operators in guards (guards require boolean operands) 4. **CHANGESETS FOR EXTERNAL DATA** — Use `cast/4` for user input, `change/2` for internal 5. **RESCUE ONLY FOR EXTERNAL CODE** — Never use rescue for control flow 6. **NO DYNAMIC ATOM CREATION** — `String.to_atom(user_input)` causes memory leak (atoms aren't GC'd) 7. **@external_resource FOR COMPILE-TIME FILES** — Modules reading files at compile time MUST declare `@external_resource` 8. **SUPERVISE ALL LONG-LIVED PROCESSES** — Never bare `GenServer.start_link`/`Agent.start_link` in production. Use supervision trees 9. **WRAP THIRD-PARTY LIBRARY APIs** — Always facade external deps behind a project-owned module. Enables swapping without touching callers 10. **MIX TASKS START ONLY WHAT THEY NEED** — `Mix.Task.run("app.config")` + `Application.ensure_all_started/1`, never `Mix.Task.run("app.start")` (boots the FULL tree: endpoint port, Oban consuming) 11. **CAPTURE LOCALE BEFORE SPAWNING** — Gettext/CLDR locale is process-local. Read it in the caller and pass explicitly; a spawned Task/GenServer starts with the default locale ## BEAM Architecture (Why Elixir Works This Way) - **Processes are cheap (2.6KB)** — Spawn liberally for concurrency/isolation - **Complete memory isolation** — No shared state, no locks needed - **Messages are copied** (except binaries >64 bytes) — Keep messages small - **Per-process GC** — No global GC pauses - **"Let it crash"** — Supervisors restart to known-good state ## Core Principles 1. **Pattern match over conditionals** — Function heads first, then `case`, then `cond` 2. **Tagged tuples for expected failures** — `{:ok, _}`/`{:error, _}` for expected errors, raise for bugs 3. **Pipe operator for data transformation** — Start with data, never pipe single calls 4. **Let it crash** — Handle expected errors, crash on unexpected ones 5. **Explicit over implicit** — Be clear about intentions ## Quick Decision Trees ### Control Flow ``` Need patterns? → case (or function heads) Multiple operations? → with Boolean conditions? → cond (multiple) or if (single) ``` ### Error Handling ``` Expected failure? → {:ok, _}/{:error, _} tuples Unexpected/bug? → raise exception (let supervisor handle) External library? → rescue (only here!) ``` ### OTP ``` Need state? ├─ No → Plain functions ├─ Simple get/update → Agent or ETS ├─ Complex messages/timeouts → GenServer └─ One-off async → Task ``` ## Quick Patterns ```elixir # Pattern match in function head def process(%{status: :active} = user), do: activate(user) def process(%{status: :inactive} = user), do: deactivate(user) # with for happy path with {:ok, user} <- get_user(id), {:ok, order} <- create_order(user) do {:ok, order} end # Task for async Task.Supervisor.async_nolink(TaskSup, fn -> work() end) |> Task.yield(5000) || Task.shutdown(task) ``` ## Common Pitfalls | Wrong | Right | |-------|-------| | `length(list) == 0` | `list == []` or `Enum.empty?(list)` | | `list ++ [item]` | `[item \| list] \|> Enum.reverse()` | | `String.to_atom(input)` | `String.to_existing_atom(input)` | | `spawn(fn -> log(conn) end)` | `ip = conn.ip; spawn(fn -> log(ip) end)` | | `unless condition` | `if !condition` (unless deprecated in 1.18) | ## References For detailed patterns, see: - `${CLAUDE_SKILL_DIR}/references/pattern-matching.md` - Pattern matching, guards, binary matching - `${CLAUDE_SKILL_DIR}/references/otp-patterns.md` - GenServer, Supervisor, Task, Registry - `${CLAUDE_SKILL_DIR}/references/error-handling.md` - Tagged tuples, rescue, with - `${CLAUDE_SKILL_DIR}/references/with-and-pipes.md` - When to use `with` and `|>` (idiomatic patterns) - `${CLAUDE_SKILL_DIR}/references/troubleshooting.md` - Production BEAM debugging (memory, performance, crashes) - `${CLAUDE_SKILL_DIR}/references/anti-patterns.md` - Common mistakes and fixes - `${CLAUDE_SKILL_DIR}/references/mix-tasks.md` - Mix task naming, option parsing, shell output - `${CLAUDE_SKILL_DIR}/references/elixir-118-features.md` - Duration module, dbg improvements (1.18+) - `${CLAUDE_SKILL_DIR}/references/elixir-120-type-system.md` - Gradual type checker, `dynamic()`, verified bugs as compile warnings (1.20+, OTP 27+) --- ## hexdocs-fetcher # HexDocs Fetcher Efficiently fetch Elixir library documentation from hexdocs.pm using Claude Code's native `WebFetch` tool. ## Usage When researching libraries, use `WebFetch`: ``` # Fetch library overview WebFetch( url: "https://hexdocs.pm/oban", prompt: "Extract the main documentation, including module overview, installation instructions, and key functions. Format as clean markdown." ) # Fetch specific module docs WebFetch( url: "https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html", prompt: "Extract the module documentation including all public functions, their specs, and examples." ) # Fetch getting started guide WebFetch( url: "https://hexdocs.pm/ecto/getting-started.html", prompt: "Extract the complete getting started guide content." ) ``` ## Token Efficiency WebFetch automatically converts HTML to markdown and extracts relevant content: | Source | Raw HTML | With WebFetch | Benefit | |--------|----------|---------------|---------| | HexDocs page | ~80k tokens | ~15k tokens | **80% reduction** | | Phoenix docs | ~120k tokens | ~25k tokens | **79% reduction** | | README | ~20k tokens | ~8k tokens | **60% reduction** | ## Integration with hex-library-researcher When evaluating libraries, fetch docs efficiently: ``` # Get library overview with focused extraction WebFetch( url: "https://hexdocs.pm/oban", prompt: "Extract: 1) Installation instructions 2) Main features 3) Basic usage example" ) ``` ## Common HexDocs URLs ``` # Library overview https://hexdocs.pm/{library} # Module documentation https://hexdocs.pm/{library}/{Module}.html https://hexdocs.pm/{library}/{Module.Submodule}.html # Guides https://hexdocs.pm/{library}/guides.html https://hexdocs.pm/{library}/{guide-name}.html # API reference https://hexdocs.pm/{library}/api-reference.html ``` ## Prompt Strategies Use focused prompts for better extraction: ``` # For API docs prompt: "Extract all public function docs with @spec and examples" # For guides prompt: "Extract the complete guide content preserving code examples" # For troubleshooting prompt: "Extract any troubleshooting sections, common errors, and FAQs" # For configuration prompt: "Extract configuration options and their defaults" ``` ## Caching WebFetch includes automatic 15-minute caching. When fetching the same URL multiple times in a session, results are cached automatically. For longer persistence, save to planning directory: ``` # After fetching, write the result to a file Write( file_path: ".claude/plans/{slug}/research/docs/oban.md", content: "{extracted content}" ) ``` ## Tidewave Alternative If Tidewave MCP is available, prefer `mcp__tidewave__get_docs` for exact version-matched documentation: ``` mcp__tidewave__get_docs(module: "Oban.Worker") ``` This fetches docs for the exact version in your `mix.lock`. ## Iron Laws 1. **NEVER fetch entire HexDocs sites** — always target specific modules or guides 2. **Use focused prompts** — generic fetches waste tokens; specify what to extract 3. **Prefer Tidewave when available** — exact version match beats generic hexdocs.pm --- ## intent-detection # Intent Detection — Workflow Routing When user describes work WITHOUT specifying a `/phx:` command, analyze their intent and suggest the appropriate workflow BEFORE starting work. **Hard guard — check FIRST**: if the message starts with any slash command (`/phx:`, `/ecto:`, `/lv:`, or any other `/command`), this skill does not apply. Follow the invoked command directly — no routing analysis, no suggestion, zero output from this skill. ## Routing Table | Signal | Detected Intent | Suggest | |--------|----------------|---------| | "bug", "error", "crash", "failing", "broken", stack trace | Bug investigation | `/phx:investigate` | | "brainstorm", "explore idea", "not sure what I need", "vague idea", "let's discuss", "how to approach" | Ideation/requirements | `/phx:brainstorm` | | "add", "implement", "build", "create" + multi-step | New feature | `/phx:plan` | | "review", "check", "audit" code | Code review | `/phx:review` | | "fix" + small/specific scope | Quick fix | handle directly or `/phx:quick` | | "refactor", "clean up", "improve" | Refactoring | `/phx:plan` (needs scope) | | "research", "how to", "what's the best" | Research | `/phx:research` | | "evaluate", "compare", "adopt", "library", "should we use" | Library evaluation | `/phx:research --library` | | "test", "spec", "coverage" | Testing | handle directly or `/phx:plan` | | Describes 1-2 file changes, < 50 lines | Small task | handle directly | | "deploy", "release", "production" | Deployment | `/phx:verify` then deploy | | "performance", "slow", "N+1", "memory" | Performance | `/phx:perf` | | "PR review", "review comments", "address feedback", "respond to PR" | PR response | `/phx:pr-review` | | "that worked", "fixed it", "problem solved" | Knowledge capture | `/phx:compound` | | "enhance plan", "more detail", "deepen" | Plan enhancement | `/phx:plan --existing` | | "triage", "which findings", "prioritize fixes" | Finding triage | `/phx:triage` | ## Behavior 1. Read user's first message 2. Match against routing table (use keyword + context signals, not exact match) 3. If match found with multi-step workflow: "This looks like [intent]. I'd suggest `[command]` — want me to run it, or should I just dive in?" 4. If trivial task (typo, single-line fix, config change): skip suggestion, just do it 5. If user already specified a `/phx:` command: follow it, don't re-suggest 6. **NEVER block the user** — suggestion only, not mandatory ## Confidence Signals High confidence (suggest immediately): - Stack trace or error message pasted → `/phx:investigate` - "Add [feature] with [multiple components]" → `/phx:plan` - "Review my changes" or "check this PR" → `/phx:review` Medium confidence (suggest with caveat): - "Fix [thing]" — could be quick or complex, suggest based on scope description - "Update [thing]" — could be small edit or refactor Low confidence (just do it): - Single file mentioned, clear change - "Change X to Y" - Configuration or dependency updates ## Complexity Signals When a task matches a workflow command, check complexity before suggesting: **Trivial signals** (suggest `/phx:quick` or handle directly): - Single file mentioned explicitly - "exclude X from Y", "add X to config", "rename", "change X to Y" - Problem + solution both stated ("X is wrong, change to Y") - One-line fix described **Complex signals** (suggest `/phx:plan` or `/phx:investigate`): - 3+ modules or files mentioned - "intermittent", "race condition", "sometimes", "random" - Stack trace with 5+ frames - "across", "all", "every" (scope indicators) **Override rule**: If user invokes `/phx:full` but task matches trivial signals: "This looks like a quick fix. Want `/phx:quick` instead, or stick with the full cycle?" ## Iron Laws 1. **NEVER block on suggestion** — If user starts explaining, just do the work 2. **One suggestion max** — Don't re-suggest if user ignores first suggestion 3. **Commands are shortcuts, not gates** — All work can be done without commands ## Routing Logic Example ``` if has_slash_command($ARGUMENTS) -> follow command directly elif has_stack_trace(message) -> suggest /phx:investigate elif matches("add|build|implement", message) and multi_step -> suggest /phx:plan elif matches("fix", message) and small_scope -> handle directly or /phx:quick elif matches("review|audit", message) -> suggest /phx:review else -> handle directly (no suggestion) ``` ## Integration This skill is consulted at session start. It works alongside: - SessionStart hook (shows plugin loaded message) - CLAUDE.md routing instructions (passive reference) - Individual workflow skills (activated by commands) --- ## liveview-patterns # LiveView Patterns Reference > **Ash projects**: Use `ash-framework` skill for `AshPhoenix.Form`. Lifecycle: `AshPhoenix.Form.validate/3` on `phx-change`, `AshPhoenix.Form.submit/2` on submit, `to_form/1` for HEEx. Do not use `Ecto.Changeset.cast/3`. Reference for building with Phoenix LiveView 1.0/1.1. ## Iron Laws — Never Violate These 1. **NO UNCONDITIONAL DB QUERIES IN MOUNT** — Mount runs TWICE. Default: `assign_async`. SEO routes: `connected?` guard + cache-backed disconnected branch (crawlers read that HTML) 2. **ALWAYS USE STREAMS FOR LISTS** — Regular assigns = O(n) memory per user. Streams = O(1) 3. **CHECK connected?/1 BEFORE SUBSCRIPTIONS** — Prevents double subscriptions 4. **EXTRACT VARIABLES BEFORE assign_async CLOSURE** — Closures copy entire referenced variables 5. **LOAD PRIMARY DATA IN mount/3, PAGINATION IN handle_params/3** — handle_params runs on EVERY URL change 6. **NEVER PASS SOCKET TO BUSINESS LOGIC** — Extract data before calling contexts 7. **CHECK CHANGESET ERRORS BEFORE UI DEBUGGING** — Silent form save = check `{:error, changeset}` first, not viewport/JS 8. **HIDDEN INPUTS FOR ALL REQUIRED EMBEDDED FIELDS** — Every required field in an embedded schema MUST have a `hidden_input` if not directly editable 9. **NEVER USE `assign_new` FOR LIFECYCLE VALUES** — `assign_new` skips the function if key exists. Use `assign/3` for locale, current user, or any value refreshed every mount 10. **MATCH `{:error, %Ecto.Changeset{}}` EXPLICITLY** — Bare `{:error, _}` merges changeset and non-changeset errors; the form silently never re-renders validation errors. Handle other errors separately ## Memory Impact | Pattern | 3K items | 10K users × 10K items | |---------|----------|----------------------| | Regular assigns | ~5.1 MB | ~10+ GB | | Streams | ~1.1 MB | Minimal (O(1)) | **Decision**: Lists with >100 items → Use streams, not assigns ## Quick Patterns ### Async Assigns (CRITICAL) ```elixir def mount(%{"slug" => slug}, _session, socket) do # Extract needed values BEFORE the closure scope = socket.assigns.current_scope {:ok, socket |> assign_async(:org, fn -> {:ok, %{org: fetch_org(scope, slug)}} end)} end ``` ### Streams for Lists ```elixir def mount(_params, _session, socket) do {:ok, stream(socket, :items, Items.list_items())} end # Insert/update/delete stream_insert(socket, :items, item, at: 0) stream_delete(socket, :items, item) ``` ### SEO Dead-Render (cache-backed disconnected branch) For public/SEO-visible routes (marketing, articles, product listings) the disconnected render IS the HTML crawlers see. Fetch from a cache there, real data on connect: ```elixir def mount(_params, _session, socket) do products = if connected?(socket), do: Catalog.list_products(), else: Cache.get_products() || [] {:ok, assign(socket, products: products)} end ``` Empty list → `