mix deps.get contacts hex.pm even when everything is already installed (the advisory check), so the everyday loops failed in offline/sandboxed environments; and the root deps target only installed app's deps, never blogex's. make setup (root) installs Hex deps for both apps plus the app's JS packages, once per clone, explicitly. check, test and test.coverage in both apps now depend on a local deps.check guard instead: every package pinned in mix.lock must exist in deps/, else fail loudly pointing at make setup. AGENTS.md documents the new target.
216 lines
11 KiB
Markdown
216 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 and the app's JS packages. 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.
|