Files
firehose/AGENTS.md
T
Firehose Bot 7fda0455c1 compile deps in make setup so cold clones stay offline
Some deps download artifacts at compile time: rustler_precompiled
fetches the mdex_native NIF from GitHub during its first compile, and
NimblePublisher pulls mdex_native into both apps. Compiling inside the
deps target keeps that traffic inside setup - the explicit user-run
network step - instead of the first make check/test on a fresh clone.
2026-10-29 18:09:42 +00:00

217 lines
11 KiB
Markdown

# 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 setup` - one-time clone setup (and after any mix.exs gains a dep):
fetches Hex deps for both apps, the app's JS packages, and compiles deps
(some deps fetch artifacts at compile time, e.g. the mdex_native NIF).
Needs network - run it yourself; agent sandboxes refuse installs.
check/test/verify never touch the network: they run a local `deps.check`
guard that fails with a pointer here when deps are missing.
- `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.