Make bun run build refuse by default instead of asking politely
Some checks are pending
build-gate / build (push) Waiting to run

"Agents never run the full build" is already a top-level heading in
AGENT-BRIEF.md, and the research that established it was three hours old.
It was violated anyway -- by a dispatch prompt that said `bun run build`
while telling the agent to read the brief first. The clean step then wiped
public/doxa-maps mid-probe and an unrelated bundle read back as "did not
mount", one step from a fictional defect in a file that was fine.

Documentation cannot fix that; an explicit instruction beats a standing rule
at the point of work. So the build refuses with exit 75 and prints the
cheaper paths (bun run tree, BUNDLE= vite build, settled-tree-gate) unless
BUILD_ALLOW=1 is set.

Opt-in rather than caller-detection, deliberately: the first draft keyed on
two environment variables that do not exist in a spawned process -- a guard
that could never fire. A probe then reported a third variable as the
discriminator; it is set in the parent too, so it marks any session rather
than a child. No environment variable distinguishes them.

Negative-tested both directions: no flag exits 75 with the refusal,
BUILD_ALLOW=1 reaches "build lock acquired".
This commit is contained in:
doxa-maps-88655c70 2026-09-23 17:48:27 +00:00
parent 0b58ab4560
commit eb95d02597
2 changed files with 55 additions and 0 deletions

View file

@ -45,6 +45,14 @@ Only push to `main` what you have actually verified — it is the branch a human
`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.
```sh
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

View file

@ -19,6 +19,53 @@ LOCK_FILE="${BUILD_LOCK_FILE:-public/.build.lock}"
WAIT="${BUILD_LOCK_WAIT:-0}"
mkdir -p "$(dirname "$LOCK_FILE")"
# ── THE COORDINATOR-ONLY GUARD ───────────────────────────────────────────────────────────────
# ★ WHY A GUARD AND NOT A DOCUMENTED RULE (measured 2026-09-23, the third instance of one shape)
# "Agents never run `bun run build`" is ALREADY a top-level heading in AGENT-BRIEF.md, and the
# research doc that established it is three hours old. It was violated anyway — by the
# coordinator, who put `bun run build` into a dispatch prompt while telling the same agent to
# read the brief first. The agent obeyed the prompt, the build's clean step wiped public/ mid-
# probe, and an unrelated bundle read back as "did not mount." That near-diagnosis was a
# fictional defect in a file that was fine.
#
# So the failure mode is NOT that the rule is missing, buried, or unclear. It is that an
# explicit instruction beats a standing rule at the point of work, every time. Documentation
# cannot fix that; only something that REFUSES the action can. The lock below already makes
# concurrent builds impossible — but it does not make an UNNECESSARY build impossible, and N
# queued agents still cost N full rebuilds, each one cleaning the tree out from under every
# concurrent reader.
#
# This guard is deliberately a SPEED BUMP, not a wall: it fails with the cheaper alternative
# printed, and any caller can proceed by setting BUILD_ALLOW=1. That is the point — the person
# who genuinely needs a full build spends one line saying so, while the agent who inherited
# `bun run build` from a careless prompt gets told what to run instead.
#
# ★ WHY OPT-IN AND NOT SUBAGENT-DETECTION (a gate that could not fail, caught before shipping)
# The first version of this guard fired on `CLAUDE_AGENT_ID` / `CLAUDE_SUBAGENT`. MEASURED: both
# are UNSET in a real subagent — I invented them — so the guard would have passed silently 100%
# of the time while looking like protection. A probe agent then reported CLAUDE_CODE_CHILD_SESSION
# as the discriminator; that is ALSO wrong, and instructively so — it is set to 1 in the
# subagent AND in the coordinator, so it marks "a Claude process", not "a child". The agent could
# not have known: it only saw its own side. A discriminator is only validated by comparing BOTH
# environments, which is why this is now opt-in rather than detection. There is no env var here
# that distinguishes coordinator from subagent; anything claiming to is an untested guess.
if [ "${BUILD_ALLOW:-}" != "1" ]; then
echo "🚫 FULL BUILD REFUSED — a full build is almost never what you want here, and its FIRST" >&2
echo " step deletes public/doxa-maps/ entirely, which tears" >&2
echo " the tree out from under every other agent reading it (measured 3+ times, incl. a" >&2
echo " 461-file phantom deletion and several false 'element did not mount' diagnoses)." >&2
echo "" >&2
echo " USE INSTEAD, cheapest first:" >&2
echo " bun run tree # regenerates manifest.json + leaf pages, NO clean" >&2
echo " BUNDLE=<name> bun x vite build # rebuilds ONE bundle, leaves the others alone" >&2
echo " node internal/settled-tree-gate.mjs # just READ the tree someone else built" >&2
echo "" >&2
echo " If you ARE the coordinator and genuinely want the one authoritative full build," >&2
echo " re-run it as: BUILD_ALLOW=1 bun run build" >&2
echo " If you are a SUBAGENT, do not set BUILD_ALLOW — ask the coordinator to build instead." >&2
exit 75 # EX_TEMPFAIL, same code as a held lock — 'not now', not 'broken'
fi
# fd 9 = the lock handle
exec 9>"$LOCK_FILE"