# Agent Instructions This project has a *zero defects policy*. When you are unsure about something, ALWAYS ask the user. # Communication When creating yaks or working on features, always ask clarifying questions to ensure understanding. Ask questions one at a time to maintain clear communication and allow for focused discussion. You run in a `nono` sandbox. Always ask the user to run commands that require privilege, like docker, sudo or package installation. # Repository structure This is an Elixir monorepo with two parts: ``` firehose/ ├── app/ # Phoenix application (OTP app: :firehose) │ ├── lib/firehose/ # Application logic │ ├── lib/firehose_web/ # Web layer (controllers, live views, components) │ ├── lib/firehose/blogs/ # Blog definitions (engineering, release notes) │ ├── priv/blog/ # Markdown posts │ └── mix.exs ├── blogex/ # Blogex library (multi-blog engine) │ ├── lib/ │ └── mix.exs ├── mise.toml # Runtime versions (Elixir, Erlang, Node) ├── Dockerfile # Deploy build; ARG pins must match mise.toml ├── scripts/check-pins.sh # Verifies the pin sync; runs in `make check` ├── .githooks/pre-push # Lens pass on push; install once with `make hooks` └── Makefile # make (check, verify, test, hooks) ``` Two blogs are configured: # Toolchain pins `mise.toml` and the `Dockerfile` ARG pins must agree; `scripts/check-pins.sh` (runs as part of `make check`) enforces this. When bumping versions, change both files in one commit. Upstream drift (newer Elixir/OTP/Debian releases) is reported by `dokku-updates firehose check` (dokku-maintenance repo). Upgrading pins or mix packages is largely unscripted: for such tasks, a Pi sub agent can be started in this directory — it reads this AGENTS.md and follows the zero-defects policy above. | Blog | Route | Description | |------|-------|-------------| | Engineering | `/blog/engineering` | Main blog | | Release Notes | `/blog/releases` | What's new in an app | # Permissions ALWAYS ask the user for permission when you want to add a fallback, or are unsure something work. The user can perform exploratory testing. We have a zero defects policy, so never continue when there are errors, warnings or test failures, even when they are pre-existing. ALWAYS report these to the user and discuss an action plan with root cause analysis. (see TDD below). # Planning we use `yx` for planning run yx --help to see options. Invoke when the user wants to make a plan, work on something for a plan or mentions 'yak' or 'yaks'. `yx list` - shows open yaks `yx add` - adds yaks `yx context "yak name"` - adds human-readable description (pipe via stdin) `yx show "yak name"` - shows yak details including context # Building This is a Phoenix Liveview monorepo, the blogging library is in blogex, the application is in 'app'. Use `make` to build and `brief` to find out more about mix. Main make targets: - `make check` - cheap static gate for both apps (no DB): app `credo --strict` + format **check** (`mix format --check-formatted`), blogex compile (warnings fatal) + format **check**, then `scripts/check-pins.sh`. It never rewrites files. - `make precommit` - alias of `check`; fast, safe to run on every commit - `make test` - both suites (app needs PostgreSQL on localhost:5432) - `make verify` - the Lens pass: `check`, then per-app heavy sensors and both suites - `make hooks` - install `.githooks/*` into this clone (once per clone) # Verification: fast loop vs Lens pass Two loops, deliberately split so commits stay cheap and pushes stay honest. **Fast loop (every commit).** `make check` is static analysis only: no database, no coverage, about a second. `make precommit` is the same target. Per app, `mix precommit` (run inside `app/` or `blogex/`) is the fuller loop: compile with warnings fatal, format, test. Run these as often as you like while working. `make check` never writes files. A formatting failure means run `make format` (both apps). The mutating loops are the explicit ones: `make format` and the per-app `mix precommit`, which still formats as you work. **Lens pass (every push, and before handing off).** `make verify` runs `check` first (cheapest), then per app: one suite run that exports its coverage, then that app's sensors (sobelow, coverage, CRAP; mutation as that yak lands), which run cheapest-first and read the export rather than re-running the suite. Measured 2026-11-01: ~11s wall (app 178 tests + blogex 156 tests + coverage floor). `.githooks/pre-push` runs `make verify` and blocks the push when anything fails. Install it once per clone: make hooks It symlinks `.githooks/*` into the clone's hooks directory; `core.hooksPath` is deliberately left alone so bd's own hooks (pre-commit JSONL flush, post-merge import) keep firing. On failure the hook prints one summary - last step started, log path, bypass - and exits non-zero; the full output is in `.git/lens-pass.log`. Deliberate override (e.g. pushing a branch you know is red): `git push --no-verify`. ## Lens practice The Lens pass is a *separate pass with its own context*. It is not a way to find work mid-feature: finish and commit what you are building, then run it. - Do not chase hook or `credo` output inline while building a feature. If something interesting surfaces, note it and file a yak; take it up in a Lens session. - A failing Lens pass blocks the push, and therefore the hand-off. Triage it; never re-run until green and never ignore it. - Adding a sensor: implement it as its own make target, add it to that app's `VERIFY_SENSORS` in `app/Makefile` / `blogex/Makefile` (cheapest first), and update the slot comments there. A gate that reports a check it did not run is worse than no gate - the `VERIFY_SENSORS` list is what gets echoed. Sensors run *after* the app's `test.coverage` step, because the coverage export they read is produced by that suite run; if a check can pass without its input, make it fail loudly (see the `coverage` target's guard). ### Triage classes | Class | Looks like | What to do | |-------|-----------|------------| | *Real bug* | behavior is wrong; a test fails for a real reason; a sensor reports a real risk | fix inline, with a test that pins the behavior | | *Environment-dependent test* | passes alone, fails in the pass; depends on ambient state (rows left behind, wall clock, ordering, network, locale) | the test is the bug: make it deterministic by construction - own fixtures, explicit clock, no ambient state as fixtures. Never retry-until-green | | *Style or design nit* | credo or format suggestion, naming, structure, "this could be smaller" | do not refactor mid-build: note it and file a yak; the Lens session decides | Reconciling this with the zero-defects policy: "never continue when there are errors" governs *what you do with a failure* - report, triage, decide - not *when* you fix it. Never silently absorb a finding, and never let a pre-existing failure ride along: if it predates your change, stop and discuss it with the user rather than working around it. Browser tests (`make playwright*`) are *external* verification and are **not** part of the Lens pass. Decided 2026-10-28 (human, yak "ratchets, deferred decisions, handoff practice"): it stays that way - Playwright is not wired into `make verify` or into either `VERIFY_SENSORS`. The targets need a running Phoenix server or the Docker Chrome container, and a gate that only runs with docker or sudo is not a gate the Lens pass can rely on. Run them as their own loop, by hand, when a change touches the browser surface. ### Landing the plane The Lens pass is what you land with, so it is the last step of every session, not a thing that only happens on push: - After any session that changed code, run `make precommit`. It is `make check` under its session-ending name and costs about a second; there is no session where it is not worth running. - Before handing off - to a human, a reviewer, or the next agent - run `make verify`, or push, which fires `.githooks/pre-push` and runs the same pass on the same tree. A hand-off note that says "tests pass" without a Lens pass behind it is a guess, not a hand-off. - A failing Lens pass is triaged into the classes above (see *Triage classes*) and **reported** in the hand-off note, with the class it fell in. A nit gets a yak; it does not get fixed inline mid-build. Never re-run until green. - `git push --no-verify` stays the deliberate override for a branch you know is red. It is never how a session lands. ## Ratchets The coverage threshold in `app/mix.exs` and `blogex/mix.exs` (the `threshold:` inside each `test_coverage/0`) is a **floor**, not a target: - After each behavior-test yak lands, re-measure both apps - `make test.coverage` then `make coverage` inside each app - and raise the floor to the new total. The convention is integer truncation of the measurement: a measured 82.61% means a floor of 82. Never above the measurement: a floor that cannot pass is not a ratchet, it is a red gate. - Never lower a floor. A legitimate drop is a conversation, not a config edit. Lowering the number, or adding an `ignore_modules` entry to prop it up, hides the behavior gap the floor exists to surface. - The record of the last measurement and its date is the comment above `test_coverage/0` in that app's `mix.exs`. Update it there, in the same commit as the number. Do not copy the totals into this file: one record, in the place the gate reads, is the whole point. Two further health numbers are baselines-to-be; neither is `make verify`'s business yet, and neither is to be invented: - **CRAP offender count.** `make crap` in one app is the sensor: a passing run prints `Summary: ... worst_score=...`, a failing run prints `High scores: N` and the offending functions. Both apps sit under the default `--max-score (30)`, and the decision (2026-10-28) not to lower it, plus the baseline offender count, live in the comment above the `crap` target in `app/Makefile` / `blogex/Makefile`. - **Mutation survivor count.** Mutation is a slot comment only in `app/Makefile` and `blogex/Makefile` ("yak: mutation testing with muex - a sensor, not a gate"), and `muex` is a dependency of neither app. Nothing has been mutated, so there is no survivor baseline; do not conjure one from a run that did not happen. When the mutation yak lands, its survivors become the baseline the same way the coverage floor did: measured, dated, recorded where the sensor lives. # Development Always fix failing tests, credo issues and format isseus. When developing new features, we apply TDD. Work Test-First, and Refactor when all tests are passing. If you believe an issue is pre-existing, stop work, and have a conversation with the user on how to address the pre-existing issue, as well as how to prevent this in the future.