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.
217 lines
11 KiB
Markdown
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.
|