Ctrl K
ComunidadGuías…

AGENTS.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 (`<designated-branch>-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 <branch> 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 <branch> origin/main`)
   **and push that reset** (`git push origin <branch>`) 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 <sha>`,
`git show <sha>:<path>`, `git diff origin/main...<sha>`): 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 <noreply@anthropic.com>`.
  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.