doxa-maps/AGENT-BRIEF.md
doxa-maps-88655c70 28b0e505b7
Some checks are pending
build-gate / build (push) Waiting to run
Point the landing recipe at the rebase hazard it can create
Landing by rebase leaves the old branch tip orphaned, and reconciling it with a
-s ours merge is how this branch got one. The rule against rebasing over such a
merge is further down the file; the recipe that creates the condition now says
so where the work happens.
2026-09-25 02:50:05 +00:00

13 KiB
Raw Blame History

AGENT BRIEF — read this once, then build

If you were dispatched to this repo, read this file first. It replaces the wall of environment rules that used to be pasted into every spawn prompt. Your prompt should now be short: what to build, which files you own, and the one assertion that would prove it.

Why this file exists: measured 2026-09-23 — agents produced 21 report docs totalling 2,079 lines and 27 scratch probes against a handful of product commits, because each spawn prompt made proving the job and building the side effect. Every agent also re-derived the same environment from scratch. That discovery tax is now paid once, here.

The rule that matters most

Ship a commit, then write two sentences. Not the reverse. The artifact is the diff. A long, honest report attached to no commit is the failure mode this file exists to end.

Land it on main. A local commit is invisible — the Driver watches main on the Forgejo repo, so work that stops at git commit, or at a side branch, is not delivered. Work in your OWN worktree off main, then land on main through the merge queue (main-capture). Pushes to main without it are REJECTED by the server: several agents share one Forgejo account, so the lock is the only thing that serializes them.

# Credentials: route git through the host's askpass helper. NEVER put a token in a URL, a
# command line, or .git/config, and never let git prompt (a prompt hangs a headless agent).
export GIT_ASKPASS="$(dirname "$(command -v main-capture)")/forgejo-askpass"
export FORGEJO_PASS_ENTRY=vivarium/forgejo-certified-sandbox GIT_TERMINAL_PROMPT=0
unset SSH_ASKPASS DISPLAY

git fetch origin && git rebase origin/main        # you resolve every conflict, not main
                                                  # ...UNLESS this branch has ever recorded a
                                                  # `-s ours` merge — then MERGE, never rebase.
                                                  # See "Never rebase a branch that has ever
                                                  # recorded a `-s ours` merge" below; landing by
                                                  # rebase and reconciling the old tip with
                                                  # `-s ours` is what puts one there, so the very
                                                  # next landing on the same branch is the one
                                                  # this catches. `git log --merges` tells you.

if main-capture capture --wait; then              # --wait retries with backoff while held;
                                                  # release ONLY a lock you actually took
  main-capture scan && git push origin HEAD:main
  main-capture release                            # ...but ALWAYS release one you took
fi

Release is not owner-checked: it deletes whoever's lock is there. So the two rules pull in opposite directions and the if satisfies both. The older one-liner capture && scan && push ; release runs release even when capture FAILED because someone else held the lock — and freed THEIR lock mid-landing (measured 2026-09-24). Never pipe capture into tail/grep either: the if would then test the filter's exit code, not the capture's.

Grade the push by its EXIT CODE (${PIPESTATUS[0]} if you pipe it), never by grepping its output: the word main also appears in the rejection line, and a grep once reported 8 rejected pushes as landed. After pushing, git fetch origin and confirm your commit is on origin/main.

Only push to main what you have actually verified — it is the branch a human is looking at. The old doxa-maps-plugin-studio branch is retired; do not push to it.

Do not run the full build

bun run build takes minutes, and its first step deletes public/doxa-maps/ — which wipes the output every other agent is using. Use the dev server; it hot-reloads.

This is now ENFORCED, not just asked. bun run build refuses with exit 75 unless you set BUILD_ALLOW=1. If you hit that refusal it is telling you the truth — take one of the cheaper paths it prints rather than reaching for the flag. Only the coordinator, running the single authoritative build for a wave, should set it. This rule was documented here before it was enforced and got violated anyway, by a dispatch prompt that said bun run build while telling the agent to read this file first. An explicit instruction beats a standing rule at the point of work, so the build now refuses instead of asking.

bun x vite --port <YOUR OWN PORT> --strictPort     # e.g. 5299, 5399, 5499 …
BUNDLE=<name> bun x vite build                     # one bundle's compiled output; does NOT clean

--strictPort is mandatory. Vite silently moves to the next free port otherwise, and five gates have 5173 hardcoded (check-facet-changes-map, plugin-gallery-smoke, smoke-studio, generate-gallery, check-scrim-covers-viewport) — so a drifted server makes a gate talk to somebody else's page and report a confident wrong answer with no error.

Before you believe a measurement

Symptom Check this first
"it didn't mount" / "0 elements" bun run check:sri — a single-bundle build leaves stale SRI in leaf pages and the browser then refuses to execute the script. Looks identical to a mount failure.
bundles missing, files deleted, counts wrong bun run check:settled — graded by exit code. It reads the build lock, which knows the queue; pgrep samples an instant and will tell you 0 builders while one is queued.
a gate "failed" Re-run it un-suppressed. A command killed by a short timeout is not a failure, and these gates print teaching prose after the verdict, so `
a selector finds nothing in a composite Shadow DOM does not pierce. Use a recursive shadowRoot walker, not document.querySelector.
a probe's verdict, in EITHER direction Read the selector and the denominator out of the source; never invent them. An invented selector fails a working feature, and a count the page prints about itself passes a broken one. Measured three times in one flight: .gm-sug returned 0 for every search query while search worked fine; a chart was "wrong" only because the probe counted all 5,563 catalogue codes when the chart counts the 5,067 spoken by a pin; and a table probe read its own "234 rows" caption and passed an entirely empty body. Assert on rendered content — rows and cells — and grep the component for the real class names first.

Facts that are easy to get wrong

  • This box HAS working WebGL via SwiftShader — verify:map-paint passes 11/11 with live GL at 1280×800. Any instruction saying "this VPS has NO WebGL" is stale; you may assert painted canvases and real zoom. Launch flags are in internal/verify-map-paint.mjs.
  • Playwright must run from inside this repo — node_modules is here; a script in /tmp cannot resolve it.
  • serve-strict takes --directory, not --root, and returns 404 for a directory URL by design (production parity). Ask for /index.html explicitly.
  • pray.doxa.life needs a browser User-Agent from scripts; a default script UA gets 403.
  • root-dx-life is a READ-ONLY clone and not even a git repo — zero edits, ever, no safety net.
  • Scratch files go in _qa-cycle/, never in this repo.

House style

  • Material LIGHT only: #f3f3f1 surface · #fff · #3b463d text · #92b195 accent · #d1d4cd border · #6d766e muted. No dark mode, no prefers-color-scheme: dark.
  • No backticks inside shadow-CSS template literals — this broke the build twice. Use string concatenation: '.x{display:block}' + '.y{…}'.
  • Keep both synced CSS copies byte-identical (check:synced-css).
  • Every visible button needs an accessible name (check:a11y-button-names).
  • Imitate before innovate. Where DX-life's designer already made a decision — radii, spacing, type scale, colours — port it faithfully rather than substituting our tokens.
  • Commit messages are product-only: no AI or authorship trailers.

How much verification is enough

Size it to the blast radius. This is a judgement, and over-verifying is a real cost, not a virtue:

Change Prove it with
one bundle's own CSS / copy the dev server + a screenshot you actually read
one bundle's logic a unit-style assertion on the function, or one browser check
anything in library/ name every consumer (grep) and re-check at least two of them — a change here reaches every bundle
layout / scroll / wheel behaviour the three-clause conjunction, via bun run check:fits-screen

State the one assertion that would falsify your change, run it, and report the number. You do not owe a complete audit.

★ Read the image when the deliverable is visual. Numbers have certified visually broken output in this repo more than once — a green generator is not a look-at-it.

A pipe hides a failure

A pipeline's exit status is its LAST command's. So this publishes a STALE bundle — new source, old .js — and says nothing:

BUNDLE=x bun x vite build 2>&1 | tail -2 && bun run tree     # tail succeeded, so && proceeds

Measured on this machine, all three states:

Form On a failed build
build 2>&1 | tail -2 && next reports success and runs next
set -o pipefail before it stops, correctly
build >/dev/null 2>&1 && next stops, correctly — no pipe, so the status is the build's

Every gate stays green afterwards, because check:sri proves the leaf and the bundle agree with each other, not with the source. The same shape bit a git merge in this repo: if ! git merge … | tail tested the pipe, so a conflicted merge read as clean.

Rule: never put a build or a merge on the left of a pipe you then branch on. Redirect instead of piping, or set -o pipefail first. If you want short output AND a reliable status, capture to a file and tail the file afterwards.

And capture the status into a variable, not $? later

$? is a single slot that EVERY command overwrites — including the harmless one somebody adds months later for readability. Measured here:

some_command > log 2>&1
sed -n '1p' log                 # redact a token, rotate the log, echo a heading — anything
if [ "$?" -ne 0 ]; then ...     # tests SED, not some_command. A failure reads as success.

The durable form names it on the very next line, and branches on the name:

some_command > log 2>&1
rc=$?                           # nothing may come between these two lines
sed -n '1p' log                 # now insert whatever you like
if [ "$rc" -ne 0 ]; then tail log; exit 1; fi

Both were run: with the sed in between, the bare $? reported success on a command that had failed; rc still read 1. This is the same family as the pipe above — a status you read one step too late is a status about something else — and it matters most in exactly the scripts that redact a token before printing a log, because that redaction looks harmless.

Landing on main: the shape of the lock

main is shared and serialized by main-capture. The ordering is load-bearing and BOTH obvious forms are wrong — each was tried, and each broke something:

Form What it does wrong
capture ; scan && push ; release ; runs release even when capture failed — so a non-owner frees the actual owner's lock. This happened: it dropped another agent's hold while they were mid-landing.
capture && scan && push && release && skips release when push fails — the branch stays protected by a lock nobody will now drop, and everyone else is wedged.

Release must be conditional on your capture succeeding, never on your push succeeding:

main-capture capture --wait && { main-capture scan && git push origin HEAD:main ; main-capture release ; }
# or, equivalently:
if main-capture capture --wait; then main-capture scan && git push origin HEAD:main; main-capture release; fi

Capture failed → release never runs, so you cannot free someone else's lock. Capture succeeded → release always runs, so a failed push cannot wedge the branch.

Two more rules that cost real work to learn:

  • Hold the lock THROUGH fetch-merge-push, not just the push. Capturing, releasing, then merging means you merged against a main someone else has already moved.
  • Never rebase a branch that has ever recorded a -s ours merge. Replaying duplicates the recorded ancestry. Merge instead. On conflicts in generated public/, take main's tree and regenerate from source rather than hand-resolving built bytes.

Where the hard-won knowledge lives

Read these only when they bear on your task; do not read them all:

  • _qa-cycle/02-builder-cycle/lessons-learned.json — stories with costs attached
  • _qa-cycle/02-builder-cycle/anti-patterns.json — smells to avoid
  • _qa-cycle/02-builder-cycle/best-practices.json — practices that proved themselves
  • _qa-cycle/01-ideation-research/VISION-the-seven-plugins-are-one-argument.md — what the product is for, and why the dashboard is deliberately shorter than a viewport