On 2026-09-29, 41 serve-strict processes had outlived the gates that started them: 279 MB resident,
the oldest 5.3 days, spread across eight worktrees. A gate that crashes, is killed, or is refused by the
browser-slot guard's process.exit cannot run its own srv.kill(), and _serve-reaped's handlers cover
signals and normal exit but not a SIGKILLed parent.
serve-strict takes --exit-with-parent: it records the pid that started it and exits within 2 s once
that process is gone. Opt-in, so a server someone deliberately left running is never taken down.
Every internal/ gate and tool that spawns it now passes the flag (14 files, plus _serve-reaped, the
shared helper). Proven: a parent that spawns the server with the flag and exits abruptly (as the
guard does) leaves it alive for a moment and gone within 3 s.
The orphans already running belong to other worktrees' runs and are left to their owners; plugin-local
scripts that spawn serve-strict should pass the flag too.
Every reference to the Driver in internal/ is reworded the same way as in
library/: the reason stays, the speaker and the quote go, an open decision
reads "provisional, pending a product decision", and agent names and decision-
sheet rows are dropped. Where a gate printed the word, its message now names
the reviewer or the ruling instead.
Three spots were code, not comments, and changed with their callers:
- the element census's allowlist field `driver` is now `ruling`, in the gate
and both presets (same validation: match, ruling, date and card required);
- check:i18n-never-translated's DRIVER-GATED marker is DECISION-PENDING, in
the entries and in the regex that counts them (its output is otherwise
byte-identical);
- the package.json description of the census says "ruling".
public-hygiene-check keeps its two patterns for the word: they are what keep
it out of the served folder.
Only the browser seat runs browsers, and only inside internal/browser-slot.sh.
That rule was remembered, not enforced: an agent ran a gate it took for a
headless source check and it launched Chromium outside the slot.
internal/lib/playwright-guarded.mjs is now the one way a repo file reaches
playwright: its launch(), launchPersistentContext(), launchServer(), connect()
and connectOverCDP() exit 75 (NOT MEASURED) with a message naming
browser-slot.sh unless DOXA_BROWSER_SLOT is set, and browser-slot.sh exports it
for the command it runs, so a bash -c grandchild inherits it. All 72 files that
imported playwright switch to it (internal/lib/browser.mjs and 64 internal gates;
plugin-designs scripts: doxa-research-map 3, my-upg-100-list 2,
research-map-plus-grid 2, research-dashboard 1): a mechanical import swap.
check:browser-gates-guarded (gates:pre) fails a direct import;
bun run browser-gates lists the 97 scripts that launch a browser. isBrowserGate
moves to internal/lib/browser-gates.mjs so the runner and the check share it.
A sanctioned entry only excused OUR string, so the other half of the same control — their "Show
Filters" beside our sanctioned "Hide Filters" (our panel is open on load) — still counted as an
omission. An entry may now carry `theirs` (+ `theirsFlags`): their string for the same control, under
the same Driver citation. It is excused only while OUR half is on the page; if our control vanishes,
their string is an omission again, so a pair can never hide a real gap.
Measured on the live research page with simpleresearch's preset (people-groups-grid): as committed,
THEIRS-ONLY 12 including "Show Filters"; with `"theirs": "^Show Filters$"` added to the Hide Filters
entry, THEIRS-ONLY 11 and "Show Filters" reported as the other half of the sanctioned pair. The preset
itself is simpleresearch's to update.
The output tree's name has always been configurable (vite.doxa-maps-build-config.json "name"),
but 61 tooling files ignored it and hardcoded 'doxa-maps' (181 literals), so a rename would have
moved the build while the gates kept reading the old folder.
- internal/_tree-name.mjs reads it once (validated as one URL-safe path segment; falls back to the
historical name) and exports TREE_NAME / TREE_NAME_RE; `node internal/_tree-name.mjs` prints it
for shell scripts.
- A codemod rewrote 171 literals in internal/ to TREE_NAME; the regex literals, messages with
backticks, atomic-public-swap.sh (fails loudly if the name cannot be read), build-lock.sh and
vite.config.js (its duplicate readMfeName is gone) were done by hand.
- The name is unchanged: old and new tooling produce all 1408 files under public/ byte for byte.
Code lines still naming the tree in internal/ + vite.config.js: 2, both intended (the helper's
fallback, the private-repo-name pattern in public-hygiene-check).
This renames nothing visible; the eventual rename becomes a one-line config change.
Every clone-fidelity gate compares properties of NAMED selectors, so none of them can see an element
that exists on only one side; regions found an extra control and an invented chart by eye, past eight
green gates. This executes the Driver's binary scope test (promoted from regions' probe).
- Harvests every visible text node through every shadow root on both sides and diffs the sets:
OURS-ONLY unaccounted (scope violation) and THEIRS-ONLY (omission) fail; SANCTIONED entries must
carry the Driver's words, date and card or the preset is refused; same-label digit drift is
reported, not failed.
- The negative control is mandatory: a known ours-only string must land in the ours-only set, or the
run prints CENSUS IS BLIND and exits 75. An unreachable live page is also 75.
- Presets live in internal/element-census/<name>.json; ad hoc via --leaf/--live/--neg.
- Measured: regions passes (list 229 vs 228, the only difference its sanctioned poster toggle; Africa
97 vs 97 with 3 live-vs-snapshot figures). With the allowlist removed the toggle FAILS as
unaccounted; a negative-control string not on the page gives CENSUS IS BLIND, exit 75.