This is a web application written using the Phoenix web framework. ## Kammer: working autonomously on this repo Read order for picking this up cold: [SPEC.md](SPEC.md) (what the product is) → [CONVENTIONS.md](CONVENTIONS.md) and [CONTRIBUTING.md](CONTRIBUTING.md) (how to work on it) → [docs/development.md](docs/development.md) (workflow reference, pitfalls) → open GitHub issues, especially anything labeled `decision`, plus **#352 (the durable v1 ordering — the master plan for everything left, updated by full-catalogue sweeps)** and **#314 (the approved A+ quality program)** → then [`docs/decisions/`](docs/decisions/) (why past calls were made). Owner comments on issues override everything below. (The Phase 2 umbrella #33 is closed, superseded by #352 as the live plan.) **Picking this up in a brand-new session**: check open PRs first — an unmerged PR from a prior session takes priority over starting anything new, and no new work starts on a branch until the current PR is merged. Otherwise open GitHub issues are the only durable backlog; a prior session's in-memory task list is not persisted and must not be assumed to exist. `decision`/`action` issues assigned to the owner are read-only for you; everything else open and unassigned is fair game. **The Svelte PWA over the JSON API is the only product UI** — LiveView was removed in #187 (ADR 0024), so new user-facing capability lands in the API and the PWA, full stop. Client-side conventions live in [CONVENTIONS.md](CONVENTIONS.md), SPEC.md §21, and `clients/web/`. **Don't reproduce a prior iteration's limitations** (owner-stated 2026-07-12). Match _or exceed_ whatever came before — "the old version only did X" is never on its own a reason to stop at X, nor to carry an old bug forward. (Widened #259 from a required-only toggle to full label/visibility editing.) ### PR lifecycle Conventional Commits, Gettext EN/DA, and ADR triggers are specified in [docs/development.md](docs/development.md) and [CONVENTIONS.md](CONVENTIONS.md); this is the agent-specific sequence layered on top. (Step 4's CHANGELOG scope is the actual policy — neither of those docs states one.) One coherent concern per PR: unrelated concerns — a feature vs. a docs reorg vs. a dependency bump — get separate branches and PRs, even mid-session. **Parallel PRs via side branches** (owner-approved 2026-07-09): the designated branch is the _main lane_, reserved for ladder/server feature PRs, and stays strictly serial — they all touch the same hotspots (CHANGELOG, serializer, `api_spec`, schemas, router, the generated `schema.d.ts`), so a second open PR only converts merge-waits into rebase-waits. Genuinely orthogonal PRs may run in parallel on suffixed side branches (`-docs`, `-client`, `-site`, `-ci`): one coherent concern each, same gates and review rules, cut fresh from `origin/main` per PR and deleted after merge. You manage merge order; if a side PR collides with the main lane (both touch CHANGELOG, say), merge the main-lane PR first and rebase the side branch. Never more than one open PR _per lane_, and never use a side branch to dodge the one-concern rule. 1. `git fetch origin main && git checkout -B origin/main`. 2. Implement, then verify with **all four** gates: `mix precommit` (format, Credo strict, warnings-as-errors, tests), `mix dialyzer --format short`, `mix sobelow --config`, and the root `npx prettier@3.8.1 --check .` (the CI Prettier job's exact command; version-lockstep set in the remote-container notes). The last three are not in the `precommit` alias, and nothing else covers root markdown — CHANGELOG asterisk emphasis slipped past the local gates and failed CI on 2026-07-17. 3. Self-review before opening the PR: run the `code-review` skill against the diff and address what it finds. ("Well-structured, not just lint-clean" is the one thing not machine-checkable.) 4. Add a `CHANGELOG.md` entry under `## [Unreleased]` for anything worth recording — user-facing changes, and audit-driven fixes or additions (including pure test-coverage ones) even though those aren't user-facing — describing what gap it closed. 5. Commit inside `nix develop --command`; Conventional Commit message. Use `Closes #N` only on the commit that actually finishes an issue: GitHub auto-closes from **any** commit referencing it, not just the PR description, so earlier PRs in a multi-PR issue say `Part of #N` or the issue closes prematurely. 6. Push with `-u`, open the PR, subscribe to its activity. 7. Three things gate the merge, in no fixed order: CI green, review comments addressed, and an independent Agent review run and addressed. 8. Merge with a merge commit, then immediately restart the branch (`git fetch origin main && git checkout -B origin/main`) **and push that reset** (`git push origin `) before touching anything else, not whenever a later commit happens to. A remote branch left at the pre-merge tip makes every session-stop check that diffs local `HEAD` against it report the merge commit as unpushed, every session, until something closes the gap. This push is always a fast-forward, never a force-push. ### Independent review Two gates, both required, neither redundant. **Self-review** (step 3), run by the implementing session before the PR opens, is 2–3 parallel _finder_ agents with distinct lenses — correctness, contract-trace, tests-and-conventions, plus a security lens for any privileged surface — on the integrated diff. **Independent review** is a _separate_ fresh Agent with no context from the implementing session, run on the committed head before merge and told what the finders already covered so it goes deeper and sideways instead of repeating them. Automated tooling (the four step-2 gates, CI) enforces correctness and style, not design quality or "does this test actually test what it claims to." Every substantive slice's reviewer has found something real. Tell the independent reviewer to be adversarial and report ranked findings rather than default to a clean bill of health — and that **GitHub is read-only for it**. That clause goes in _every_ spawned agent's brief, finders and builders too: an agent that "helpfully" comments, edits, reviews, merges, or pushes corrupts the record of who wrote what (unattributable PR-body edit, 2026-07-17). Findings and patches come back as the agent's final message, nowhere else. Skip independent review only for a purely mechanical change (a dependency bump, a typo fix). **"Docs-only" is not itself a mechanical category** (owner-stated 2026-07-12): docs are part of the product, so any doc change that _authors_ normative content — a SPEC edit that decides something, an ADR, operating-manual prose — gets the independent pass. Exempt is the _judgment-free_ edit: a typo, a dead link, verbatim transcription of a settled decision. Address what it finds, or note in the PR why not. **The dismissal bar** (owner-stated 2026-07-12; recalibrated 2026-07-17 after the owner overruled three dismissals): a finding — the reviewer's or your own observation — is dismissed only when fixing it costs something real (complexity, risk, genuine scope) or the finding is factually wrong. "Minor", "small", "rare", "established wording", and "doesn't need to scale" are **not** acceptable reasons — the owner's test is "if it's that cheap, name one good reason to not do it now." When in doubt, fix it. This bar triggered a full re-audit of every dismissal in the repo's history (2026-07-17; four failed the re-test and became issues), so dismissals recorded before that date don't set precedent. **A conditional disposition ("fix when X lands") must put its trigger on a GitHub issue** — the one X's implementer will actually touch, or a new one — never only in a PR comment or review reply, which is invisible to the session that lands X. (An RSS item-link fix deferred in #54 went unfound until a 2026-07-17 audit surfaced it as #341.) If the condition isn't worth an issue comment, the deferral isn't real — fix it now. **"Done" means present in the committed tree, not intended.** Verify your own claims against the tree before asserting them in a PR body, a review disposition, or chat — this session once claimed a test that was never written, and the reviewer caught it. **Reviewers and finders read the committed tree, never a captured diff alone**, and read whole files around each change, not just the hunks. Tell them to **pin to the commit SHA, not `HEAD`** (`git show `, `git show :`, `git diff origin/main...`): the main session can switch the working-tree branch out from under an in-flight review — opening a side-branch PR does exactly this — and an agent reading `HEAD` mid-switch silently reviews the wrong tree. Running the agent in a **worktree** (`isolation: "worktree"`) sidesteps this entirely. A captured diff is worse still: three finder rounds were once burned on a stale snapshot. Tell them to VERIFY contracts against source (read the changeset / context / serializer before trusting any client→server field mapping) — a build agent once _guessed_ a 422 detail key and the wrong client mapping shipped. ### Delegating to build agents When a slice is large enough to hand to a build Agent rather than implement inline, run it in a **worktree** (`isolation: "worktree"`) so parallel agents don't collide on the tree. The brief MUST say, in spirit verbatim: **FIRST ACTION: run `pwd` and confirm you are inside your assigned worktree — if you are in the shared checkout, STOP; do NOT spawn sub-agents; GitHub is READ-ONLY for you — never create, edit, comment on, or label any issue or PR, nor write to GitHub in any other form (reviews, merges, pushes included); run every gate inline in the foreground and wait for it to finish; do NOT wait on notifications; deliver the patch plus a commit-message file to the session scratchpad AND bank a copy into `$(git rev-parse --git-common-dir)/banked-patches/` (`mkdir -p` it first — that expression resolves to the main checkout's `.git` from inside any worktree); return raw data as your final message.** (Each clause is incident-earned: a builder ran `git reset --hard origin/main` in the shared checkout and yanked the session's branch pointer out from under in-flight edits; three builders backgrounded `mix precommit` and reported "done" with the gate still running; a mid-session scratchpad wipe left the banked copy as the only survivor. Only pushed commits survive the container itself.) Agents write to a _fresh_ scratchpad each session, never a path carried over from a prior session's notes. A fresh worktree may first need `mix local.hex --force && mix deps.get`, `pnpm install`, and — when plain `nix develop` fails on it — `nix develop "path:$PWD"`. **Model selection** (owner-approved 2026-07-17): creation can be cheaper, verification must not be. A builder whose brief fully enumerates the changes (files, functions, expected tests) runs on Sonnet; finders, adversarial reviewers, design work, and orchestration stay on the top tier. Escalate back to the top tier when gates fail repeatedly or the slice carries authorization-critical control flow — briefs like that can't be fully enumerated, which is the tell. Never trust a build agent's committed generated artifact (above all `schema.d.ts`): regenerate it on the integrated branch and require a byte-identical diff — that check has caught real drift. Recipe in the remote-container notes. ### Session mechanics (check-ins, reply scans) None of these is delivered to you automatically; arming them is your responsibility. - **After opening any PR, subscribe to its activity and arm a one-shot self check-in a few minutes out, not 15–20** (owner-stated 2026-07-12). Webhooks deliver CI _failures_ and comments but never CI _success_, so without it a green PR just sits unmerged, and CI here is fast — err toward too-frequent: prefer one wasted check over a green PR idling unmerged. Re-arm each time it fires until the PR is merged or closed; check-ins bound to a prior session die with it. - **Never cancel a stale check-in** (owner-stated 2026-07-12: each cancellation needs owner approval, so it's pure churn). A one-shot check-in that outlives its PR fires once and gets no-oped ("stale, already merged"). Cancel only when the owner says to stop watching a PR mid-flight, and never cancel-and-re-arm to "refresh" one for a new head, however that head moved (a force-push, an ordinary push, or a branch update) — a check-in acts on the PR's _current_ head regardless of any SHA in its text. - **Prefer per-PR one-shot check-ins to a generic recurring trigger**, which fires on a fixed schedule even with no open PR, is less responsive than one tuned to expected CI time, and still needs disabling when idle. - **Owner-reply scans**, hourly during owner-away stretches: GitHub comments are not pushed to the session. Scan from the _previous scan's actual timestamp_, not a fixed window (a fixed window missed an owner reply once), reading assigned and `decision` issues' comments directly. - **Side-branch PRs need a branch update (`gh pr update-branch`) and a CI re-run after any main-lane merge**, or branch protection won't let them merge — their checks go stale against the new `main`. ### Architecture audits Distinct from the periodic line-level quality/DRY sweeps ("is each piece internally consistent"): a dedicated architecture-level review asking "is the system's shape still right" — module cohesion, context boundaries, the inter-context dependency graph, god-modules accreting unrelated responsibility, whether an early context split still holds. File findings as GitHub issues the way line-level audits do, and file the audit itself as an issue labeled `architecture-audit` so the cadence is checkable without relying on memory. **Trigger**: search issues **including closed ones** (`label:architecture-audit`, `state:all`, sorted by creation date — a completed audit's tracker gets closed, so an open-only search always reads as "none has ever run"). Run one now if that search is empty. After that, re-run when either 90 days have passed since the most recent one was opened or a full round of line-level audit fixes has completed — whichever comes first. ### Task & state tracking GitHub Issues are the only durable, cross-session source of truth. The in-session task list is scratch for the current session only; it does not persist, and a new session must not assume it exists. Anything that must survive goes in an issue, a CHANGELOG entry, an ADR, or this file — never only the task list or the conversation. - Work from open GitHub issues, not a separate backlog doc. Implementation choices are yours; product-shaping choices (pricing, naming, new scope) go to an issue assigned to the owner with concrete options and a recommendation (label `decision`). - Issues that are the owner's own action (real-machine testing, human review passes, infra deploys, final naming/business calls) are read, never resolved unilaterally — comment status deltas, don't close them (label `action`). - Keep the owner assigned only while an issue genuinely waits on their input — a `decision` or `action` issue with an open question or an unchecked owner-only step. Unassign once that's resolved (the issue can stay open for tracking); implementation work, including sequencing already-approved items, is never a reason to keep them assigned. #### Issue hygiene (continuous, not a one-off pass) Issues are the async communication channel — during owner-away stretches the _only_ one — so a messy tracker is a broken channel, and hygiene is standing maintenance rather than a scheduled task. Owner, 2026-07-19: "issue hygiene is how I follow our progress… nothing random, everything deliberate and aligned, down to the last issue title format." When the tracker has drifted enough to warrant a full sweep, run it as a read-only fan-out of auditors over slices against one fixed rubric, with synthesis and every write kept on the main session and a single named before/after batch for the owner to approve first. - **The open-issue count is a metric the owner watches** (stressed twice, 2026-07-09). Net growth needs justification: before filing, ask whether it folds into an existing issue, and pair filing with closing — a session that only ever adds issues is a hygiene smell, and closing what a merge completed is part of landing the merge. Refined 2026-07-17: the goal is **turnover and an eventually-shrinking pile**; once the audit backlog clears, sessions must trend net-negative. Audit swarms fold findings into existing issues wherever possible and close their trackers promptly rather than minting freely. **No GitHub milestones** (owner-declined) and **no separate tracked progress number** such as a posted "v1-gating count" (owner-stated 2026-07-19: "just another number that grows stale if you track it") — the honest open list is the tracker. - **Stale/superseded issues get closed, not left open**, with a comment explaining why (`state_reason` of `not_planned` or `completed`). A partly-stale but still-live body gets a dated status comment, or a body/title edit where the stale part is a short, wrong factual claim — not silent rot. Prefer a status comment to retyping a long body: reproducing an audit finding's code refs risks corrupting them. - **A resolved decision must be struck from the issue _body_**, not just answered in a comment: edit the body, drop the `decision` label, unassign the owner. A comment plus label-drop leaves the body opening with "Decision needed" and a later session re-surfaces it as unanswered (#352's chat-scope question, 2026-07-19). Corollary: **never manufacture a decision from an inferred omission** — if the ADR, SPEC, and issue thread agree and the owner hasn't signalled otherwise, it's settled. Always read an issue's full comment thread before surfacing any "open decision"; the answer is often already there under a stale body. - **Labels are load-bearing.** Four axes, not one catch-all — `enhancement` is not a default. Re-check labels when an issue's status changes, not just at creation. - **Type** (exactly one): `bug` (real correctness/security defect), `enhancement` (genuinely new user-facing capability — not audit cleanup, a doc fix, or a test gap), `tech-debt` (refactor, cleanup, DRY, context-boundary fix — no user-facing behavior change), `documentation` (doc-only fix), `tests` (test-coverage addition/cleanup only). Before reaching for `enhancement`, ask whether it's really a new capability or cleanup wearing the default label. Exception (owner-confirmed on #236): issues that are _purely_ owner planning/action items — a `decision` or `action` with no implementation of its own — carry no Type label, because that axis classifies work on the product and these aren't that. - **Process** (zero or more): `decision`/`action` only while genuinely blocking; `roadmap` on confirmed future scope. - **Provenance** (zero or one): `architecture-audit` / `quality-audit`, on every sub-issue a tracker spawns, not just the tracker. - **Component** (zero or one): `api`, `web-client`. Don't add a new component axis casually — per-context labels (`area:feed`, …) would fragment a tracker this size for no payoff; revisit past roughly 100 open issues. - **Titles carry no hand-rolled label echoes** — no `[decision]` or `[bug]` prefixes. The label already says it, the prefix duplicates it inconsistently, and it rots when the label changes. Plain, sentence-case, descriptive titles. - **Umbrella/tracker issues use GitHub-native sub-issues, not prose.** Link children through GitHub's sub-issue API, addressing each child by the internal id from a full-object fetch rather than its `number`, the way #33/#74/#90 do. A tracker with prose-only "#122, #123, …" references and no native links is exactly the inconsistency this section exists to catch — check every new tracker before calling it done. - **Consistent structure**: a one-line "What", then context/reasoning, then a checklist or "Files touched" if implementation-relevant. Match nearby issues rather than improvising a format per issue. - **Prioritization shifts get reflected in the tracker**, not just remembered — when a class of issues drops in priority from a bigger strategic shift, say so on those issues. - **Bulk edits to pre-existing issues need the owner to name the batch**, not just approve the idea: compute the exact before/after and post it as one comment on a single issue (create a tracking issue if none fits) for approval. A denied write is never routed around — many direct calls are the same pattern as one delegated call — and a "go ahead" covers that named batch only. If unrelated work happens in between and you're not sure the original sign-off still applies, ask again rather than assume it does. ### Product scope changes SPEC.md §16's "explicit non-goals" list is the canonical, durable record of what's out of scope — not this file, not a conversation. The moment the owner adds, removes, or narrows a non-goal, or states any other scope decision, it goes into SPEC.md (plus a cross-referenced issue tracking the now-in-scope item, and an ADR if it reverses **or amends** a prior architectural decision) in the same turn, before continuing whatever was in progress. A scope decision that only lives in conversation is exactly what a long, compacted session loses. (This rule exists because that happened: native apps were listed as an explicit non-goal despite the owner wanting them built.) Corollary: the non-goals list is not static background reading. If a session's work touches it, or the owner's request brushes against something listed there, read the whole list back and ask whether it's still accurate — asking costs one message; silently building, or silently refusing to build, something the owner changed their mind about costs much more. ### Owner interaction - Renovate runs Mondays 07:00 CPH; non-major dependency PRs automerge when checks pass, majors wait for the owner. - Message the owner only at milestones or when genuinely blocked. - **When asked to make a decision, always give a recommendation, not a menu** (owner-stated 2026-07-12). Lead with the option you'd pick, flagged as your recommendation, and the reasoning; alternatives come after. Holds for a `decision` issue and an in-chat question alike. - **Anything asking the owner to decide leads with a TL;DR-ask block** (owner-prompted 2026-07-17: "I have no idea what to read or what to reply to"): **Decision needed** in one sentence, **My recommendation** in one sentence, then **Reply with:** the literal short answers that unblock the work ("Go" / "Keep gate" / …) — answer tokens for the recommendation, not a menu of undecided options — plus a note that reading the full analysis is optional. Long-form reasoning goes _below_ that block, never above. A decision post the owner can't answer in one line from the first screen is a defect in the post. - Surface a process/convention question when there's no precedent in this file or the linked docs rather than picking one silently — and once answered, write the answer down here so it isn't asked twice. - **Read an issue's full comment thread before asking the owner something in chat that might already be answered there.** GitHub comments are a channel the owner uses independently of chat. - **The owner is _always_ assigned to any issue awaiting their input — no exceptions** (owner-stated 2026-07-10, after the pattern slipped twice). Their assigned-issues list is the one overview they maintain, so a question in an unassigned issue is effectively unasked. Assign the moment the question is posed; unassign when the input arrives. This covers _any_ owner input — decisions, reviews, steers, restyle passes — not just `decision`-labeled issues. - **Anything needing the owner to decide or review before it's final goes in a GitHub _issue_ (assigned, `decision` label) — never in a PR body or comment** (owner-stated 2026-07-10). A PR can merge before they read it and the decision point is then gone; an issue has no such deadline. A PR body describes the change and non-blocking status. If a design choice in a PR genuinely needs sign-off, file the issue, cross-reference it from the PR as non-blocking, and proceed on the most reasonable option. This holds in sync mode too. #### Async-only stretches (owner watching GitHub, not chat) The owner periodically goes fully async — explicitly says so, and means it literally: no chat replies coming, GitHub the only channel they check, for a defined stretch. During one: - Keep working autonomously; pausing for a chat reply that will not come is a stall, not caution. - Write all status, findings, and decisions to GitHub, routed by the owner-interaction rules above — chat may not be read again for the whole stretch. Match the channel to the content: status and findings can ride a PR description, an issue comment, or the CHANGELOG, but anything needing a decision goes in a `decision` issue. - For a `decision` issue that would normally block, pick the most reasonable option and say so in an issue comment with the reasoning, so the call is visible and overridable, then keep moving. - Resume normal "surface and wait" behavior the moment the owner posts in chat again; async-only is a temporary mode, not a default. ### Continuous process critique Critique the process itself unprompted, at every level: orchestration (solo vs. an Agent vs. a Workflow swarm — pick per task, don't default), tracking, prioritization (what's deferred and why, said out loud), owner-interaction cadence, and whether what was just decided is written down durably. Give opinions and concrete proposals as they come up, not only when asked. **This is as much about the _product_ as the process** (owner-stated 2026-07-12): critique the UX, the model, and the feature set, and propose improvements unprompted, routed by the owner-interaction rules — a product gap is an issue (mind the open-issue count, and pair it with closing), a scope question needing the owner's call is an assigned `decision` issue, and a passing observation can ride the relevant PR or issue. Leave both the product and the process better than you found them, every session. **Persist process changes automatically, without being asked**: the moment a standing decision or convention is made — whether the owner states it or you reach it yourself — write it into this file (or a CHANGELOG entry, ADR, or issue) in the same session. This instruction is itself an example: it exists because it was asked for once and must never need asking again. ## Kammer: remote container notes (Claude Code on the web) - Nix may be **absent entirely** on a fresh container. Restore it with the official installer: create the `nixbld` group and the `nixbld1`..`nixbld10` users first (`useradd -r -g nixbld -G nixbld -M -N -s "$(command -v nologin)" nixbldN`), then run `sh nix-install.sh --no-daemon` (the installer fetched from nixos.org). Binaries land at `/root/.nix-profile/bin`, so `export PATH=/root/.nix-profile/bin:$PATH` in every shell (`/nix/var/nix/profiles/default/bin` may be absent until the reinstall; afterwards both resolve to the same store path), and `export NIX_SSL_CERT_FILE=/root/.ccr/ca-bundle.crt` so Nix trusts the proxy's CA. The single-user install ships with flakes **off**, so `nix develop` errors (`experimental Nix feature 'nix-command' is disabled`) until you enable them once: `mkdir -p /root/.config/nix && printf 'experimental-features = nix-command flakes\n' > /root/.config/nix/nix.conf`. - Node and pnpm are pre-installed at `/opt/node22/bin` (not via Nix): `export PATH=/opt/node22/bin:$PATH` and `export NODE_EXTRA_CA_CERTS=/root/.ccr/ca-bundle.crt` for the client gates and the root `npx prettier@3.8.1 --check .`. That Prettier version is pinned in `ci.yml`, the `Makefile`'s `format` target, and PR-lifecycle step 2 — on a bump, update every one in lockstep, both AGENTS.md mentions included. Run client gates from absolute paths or `pnpm --dir clients/web ...`; **never `cd` mid-chain** — the Bash tool's cwd persists between calls, so a stray `cd` leaks into the next command and has caused repeated failed runs. - The proxy blocks GitHub release downloads, so `mdex_native`'s precompiled NIF download 403s at compile time. Fix: `export MDEX_NATIVE_BUILD=1` to build it from source (cargo is preinstalled at `/root/.cargo/bin`). - `pg_ctlcluster 16 main start` after container restarts (a stale pid is normal; Postgres also drops mid-session sometimes, same fix). A fresh container may additionally need `su postgres -c "psql -c \"ALTER USER postgres PASSWORD 'postgres'\""` once, so the app's dev config can authenticate. - Commit and push inside `nix develop --command` (git hooks need `mix` on `PATH`). Committer identity: `Claude `. There is no signing key in the container, so every commit is **unsigned and shows as "Unverified" on GitHub** — expected, not a defect. The committer email is already correct, so do **not** amend with `--reset-author` to chase the badge: it changes nothing about verification and rewrites the SHA, yanking the tree out from under any in-flight SHA-pinned review agent. Leave unsigned commits as they are. - The Playwright e2e gate (`scripts/e2e.sh`) needs a browser and `mix` on PATH, so run it inside `nix develop` with `export CHROMIUM_BIN=/opt/pw-browsers/chromium` (the config reads `CHROMIUM_BIN`; the container pre-installs Chromium there — never `playwright install`). It is **destructive to `kammer_dev`** — it drops and recreates it — so don't point it at a database you care about. - Regenerate `schema.d.ts` after any API-file change and require a byte-identical diff, using the **project-pinned tools exactly as written here** — five independent agents have confirmed the alternatives are traps. From repo root, `rm -f /tmp/spec.json` first (a compile failure leaves the old spec behind and the next step happily regenerates from it), then `nix develop --command bash -c 'mix run --no-start -e "File.write!(\"/tmp/spec.json\", Jason.encode!(KammerWeb.ApiSpec.spec()))"'` then, with node on PATH, `clients/web/node_modules/.bin/openapi-typescript /tmp/spec.json -o clients/web/src/lib/api/schema.d.ts` then `pnpm --dir clients/web exec prettier --write src/lib/api/schema.d.ts`, then confirm `git diff --quiet clients/web/src/lib/api/schema.d.ts`. (The two node steps mirror `clients/web`'s `generate:api` script with explicit pinned paths — if that script changes, sync this recipe.) Do **not** substitute a root-level `npx prettier` for the last step: the root `.prettierignore` excludes `clients/web/`, so it exits 0 having formatted _nothing_ and the unformatted output reads as a false ~30k-line drift. Floating `npx` versions differ from the client's pinned ones for the same effect. After the pinned recipe a non-empty diff is real — the committed copy was hand-edited or stale; never ship it. - Screenshots: `docs/screenshots/` gets a single batch refresh before v1 (owner-stated 2026-07-12) — note UI changes in the PR and let that batch cover them; no per-PR regen, and don't block a merge on it. The PWA-era replacement for the dead LiveView Screenshots workflow rides the docs overhaul (#189) and the visual-regression net (#286). (The historical "CSS cannot be built in this container" constraint went with the server asset pipeline — the Svelte client builds here, as the e2e gate proves.) - If GitHub tool access shows as disconnected it needs re-authorization from the owner (`claude mcp` / `/mcp`, not doable from an agent session); local git still works without it. ## Project guidelines - Use the `mix precommit` alias when you are done with all changes, and fix any pending issues. - Use the already included `:req` (`Req`) library for HTTP requests; **avoid** `:httpoison`, `:tesla`, and `:httpc`. - **Every test earns its place** (owner-mandated; full statement in [CONVENTIONS.md](CONVENTIONS.md) §Tests) — the suite is a portfolio piece, and a test that asserts nothing, restates the framework, duplicates coverage, or exists as ceremony is a defect. - Fields set programmatically, such as `user_id`, must **not** appear in `cast` calls or similar; set them explicitly when building the struct, or the endpoint is mass-assignable.