From 71ee5d0d4fa1ecf3aab2a8d92f386e7f3fca09d1 Mon Sep 17 00:00:00 2001
From: "Mike P. Sinn"
Date: Sat, 16 May 2026 16:13:31 -0500
Subject: [PATCH 01/22] /plaintiffs: compress above-form copy to Mike's outline
(Variant 1)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Drop the entire wrongful-death-cutoff section. Reduce header to four
paragraphs that match Mike's dictated outline minus filler:
1. $36T/yr welfare claim + $170T/310M war ledger since 1900
2. 1% Treaty + military-freeze counterfactual: N years of trials,
disease eradicated by 1950, aging reversed by 1990 (+ methodology link)
3. Every disease case since 1950 + every aging death since 1990 is
misallocation harm — wrongful, attributable, registerable
4. Register anyone you know who suffered disease since 1950 or died
since 1990
Net: ~150 -> ~110 words above the form. Drops "of their employers"
joke, the standalone "It is wrongful death." closer, the duplicate
REGISTER button (form is one scroll below).
Audience: grieving family member who lost someone to disease (>=1950)
or aging-related cause (>=1990). Goal: scroll to form + register.
Theory of mind: serious numbers + named lawsuit + year-anchor that
includes their loved one + methodology link clear four blockers
(scam, will-it-do-anything, is-my-death-attributable, math-credible).
Codex preflight b0xg5gypw clean (typecheck, validate:content, focused
smoke + visual, methodology URL returns 200).
qa-passed: Codex b0xg5gypw — typecheck:fast, validate:content, focused
e2e/visual smoke, methodology link 200, no source fixes required.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../web/src/app/plaintiffs/page.logged-out.md | 10 +-
packages/web/src/app/plaintiffs/page.tsx | 117 ++++++++----------
2 files changed, 57 insertions(+), 70 deletions(-)
diff --git a/packages/web/src/app/plaintiffs/page.logged-out.md b/packages/web/src/app/plaintiffs/page.logged-out.md
index 52d1072df..3afc12943 100644
--- a/packages/web/src/app/plaintiffs/page.logged-out.md
+++ b/packages/web/src/app/plaintiffs/page.logged-out.md
@@ -14,12 +14,10 @@
## Visible Page Copy
## REGISTER PLAINTIFFS FOR HUMANITY V GOVERNMENT.
-- You pay governments [$36.5 trillion](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html) a year to promote the general welfare — i.e. maximize median healthy life years and median after-tax inflation-adjusted income. Since 1900 they spent [$170 trillion](https://manual.WarOnDisease.org/knowledge/problem/cost-of-war.html) murdering [310 million](https://manual.WarOnDisease.org/knowledge/problem/cost-of-war.html) of their employers — enough to fund [37,800](https://manual.WarOnDisease.org/knowledge/strategy/declaration-of-optimization.html) years of clinical trials at current funding levels.
-- Register anyone you love who was killed or harmed. The case in [Humanity v. Government](/humanity-v-government) seeks [$10.6 million/person](https://manual.WarOnDisease.org/knowledge/solution/court-of-humanity.html) per murdered human in damages.
-### IF SOMEONE YOU LOVE DIED OF DISEASE IN OR AFTER [1950](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html), OR AGING IN OR AFTER [1990](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html), IT IS WRONGFUL DEATH.
-- The cited math counts the time to build medical tools and clear the treatment queue, then puts the disease line at [1950](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html). Aging gets about [40](https://manual.WarOnDisease.org/knowledge/problem/cost-of-war.html) more years and lands at [1990](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html); biology was harder, not innocent.
-- IT IS WRONGFUL DEATH.
-- [REGISTER A PLAINTIFF](#register-plaintiff)
+- You pay governments [$36.5 trillion](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html) a year to promote the general welfare — i.e. maximize median healthy life years and median after-tax inflation-adjusted income. Since 1900 they spent [$170 trillion](https://manual.WarOnDisease.org/knowledge/problem/cost-of-war.html) murdering [310 million](https://manual.WarOnDisease.org/knowledge/problem/cost-of-war.html) people.
+- Had they adopted the [1% Treaty](/treaty) and frozen military spending in 1900, that money would have funded [37,800](https://manual.WarOnDisease.org/knowledge/strategy/declaration-of-optimization.html) years of clinical trials. Disease would have been eradicated by [1950](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html). Aging reversed by [1990](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html). [methodology](https://manual.warondisease.org/knowledge/appendix/parameters-and-calculations.html)
+- Every disease case since [1950](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html) and every aging death since [1990](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html) is misallocation harm — wrongful, attributable, registerable.
+- Register anyone you know who suffered disease since [1950](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html) or died since [1990](https://manual.WarOnDisease.org/knowledge/appendix/humanity-v-government.html).
### REGISTER PLAINTIFF
- FIRST NAME
- MIDDLE NAME OPTIONAL
diff --git a/packages/web/src/app/plaintiffs/page.tsx b/packages/web/src/app/plaintiffs/page.tsx
index 312018223..4f001f22e 100644
--- a/packages/web/src/app/plaintiffs/page.tsx
+++ b/packages/web/src/app/plaintiffs/page.tsx
@@ -3,7 +3,6 @@ import {
CUMULATIVE_MILITARY_IN_GOVT_TRIAL_YEARS,
CUMULATIVE_MILITARY_SPENDING_FED_ERA,
LOST_PROSPERITY_LIFETIME_DAMAGES_PER_CAPITA,
- WAR_TRIAL_REDIRECT_AGING_LAG_AFTER_DISEASE_CONTROL_YEARS,
WAR_TRIAL_REDIRECT_AGING_PLEADING_CUTOFF_YEAR,
WAR_TRIAL_REDIRECT_DISEASE_PLEADING_CUTOFF_YEAR,
WAR_DEATHS_SINCE_1900,
@@ -178,82 +177,72 @@ export default async function PlaintiffsPage({
param={CUMULATIVE_MILITARY_SPENDING_FED_ERA}
/>{" "}
murdering{" "}
- of
- their employers — enough to fund{" "}
- {" "}
- years of clinical trials at current funding levels.
+ {" "}
+ people.
- Register anyone you love who was killed or harmed. The case in{" "}
+ Had they adopted the{" "}
- {humanityVGovernmentLink.label}
+ 1% Treaty
{" "}
- seeks{" "}
+ and frozen military spending in 1900, that money would have
+ funded{" "}
{" "}
+ years of clinical trials. Disease would have been eradicated by{" "}
+
+ . Aging reversed by{" "}
+
+ .{" "}
+
+ methodology
+
+
+
+ Every disease case since{" "}
+ {" "}
+ and every aging death since{" "}
+ {" "}
- per murdered human in damages.
+ is misallocation harm — wrongful, attributable, registerable.
+
+
+ Register anyone you know who suffered disease since{" "}
+ {" "}
+ or died since{" "}
+
+ .
-
-
- If someone you love died of disease in or after{" "}
-
- , or aging in or after{" "}
-
- , it is wrongful death.
-
-
- The cited math counts the time to build medical tools and clear the
- treatment queue, then puts the disease line at{" "}
-
- . Aging gets about{" "}
- {" "}
- more years and lands at{" "}
-
- ; biology was harder, not innocent.
-
-
- It is wrongful death.
-
-
- REGISTER A PLAINTIFF
-
-
-
From e7d5dce5d47c52bdbcfabee63b527d709a16f1d1 Mon Sep 17 00:00:00 2001
From: "Mike P. Sinn"
Date: Sat, 16 May 2026 16:18:13 -0500
Subject: [PATCH 02/22] hooks: enforce codex-background, copy-review,
theory-of-mind on copy edits
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Three new PreToolUse hooks promote text rules to active enforcement
(per feedback_promote_violated_text_rules_to_hooks):
1. enforce-codex-background.mjs — BLOCK Bash `codex exec`/`codex review`
without run_in_background:true. Rule lived in
.claude/codex-delegation.md:7 as plain text; got violated this
session (Mike had to manually background a 10-min foreground
dispatch). Now active.
2. enforce-copy-review-before-commit.mjs — BLOCK `git commit` when
staged diff touches user-facing copy files unless the current turn
has shown BEFORE/AFTER + called AskUserQuestion with predicted
complaints + freeform Other. Sister hook to review-loop-gate
(which is post-deploy; this one is pre-commit).
3. enforce-theory-of-mind-on-copy-edit.mjs — BLOCK Edit/Write/MultiEdit
to user-facing copy files unless current-turn chat contains
Audience + Goal + Theory-of-mind reader simulation phrases.
Sister to enforce-audience-and-goal-on-ui-dispatch (which only
covers Codex dispatches; this fills the direct-edit gap).
All three triggered by Mike escalations this session after I shipped
copy edits without the pattern they encode. Forward-only: no
project-source changes required.
qa-passed: skipped — pure meta-config (.claude/hooks/, .claude/settings.json)
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.claude/hooks/enforce-codex-background.mjs | 76 +++++++
.../enforce-copy-review-before-commit.mjs | 177 +++++++++++++++
.../enforce-theory-of-mind-on-copy-edit.mjs | 211 ++++++++++++++++++
.claude/settings.json | 25 +++
4 files changed, 489 insertions(+)
create mode 100644 .claude/hooks/enforce-codex-background.mjs
create mode 100644 .claude/hooks/enforce-copy-review-before-commit.mjs
create mode 100644 .claude/hooks/enforce-theory-of-mind-on-copy-edit.mjs
diff --git a/.claude/hooks/enforce-codex-background.mjs b/.claude/hooks/enforce-codex-background.mjs
new file mode 100644
index 000000000..8e8bd69d9
--- /dev/null
+++ b/.claude/hooks/enforce-codex-background.mjs
@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+// enforce-codex-background.mjs
+//
+// PreToolUse hook on Bash: when the command invokes `codex exec` or
+// `codex review`, REQUIRE the Bash tool call to carry
+// `run_in_background: true`. Foreground Codex dispatches block the
+// orchestrator for minutes while Codex churns — and the rule has lived
+// in `.claude/codex-delegation.md:7` as plain text since the protocol
+// was written. Plain-text rules lose to active enforcement.
+//
+// 2026-05-16 trigger: Mike, verbatim — *"do you remember what our
+// workflow is? Where you fucking do something and then give me the
+// links that I have to review, ask me questions about it and stuff?
+// And then you always delegate everything to Kodak's agents in the
+// background. Do we have that documented in hook or something,
+// something, somewhere that will force you to do it?"* — after I
+// dispatched a Codex preflight in foreground (10 min Bash timeout)
+// for a copy-only commit. He had to background it himself by
+// interrupting.
+//
+// Bypass: none. If the dispatch is truly short-lived (<30s) and the
+// orchestrator needs the result inline, dispatch with a Monitor watcher
+// or refactor the work — never bypass.
+//
+// Related: feedback_promote_violated_text_rules_to_hooks.md.
+
+import { readFileSync } from "node:fs";
+
+try {
+ const raw = readFileSync(0, "utf-8");
+ if (!raw || !raw.trim()) process.exit(0);
+
+ const hookData = JSON.parse(raw);
+ if (hookData?.tool_name !== "Bash") process.exit(0);
+
+ const command = String(hookData?.tool_input?.command ?? "");
+ if (!command) process.exit(0);
+
+ // Skip non-codex first tokens (mirrors enforce-codex-protocol.mjs).
+ const firstToken = command.trim().split(/\s+/)[0] ?? "";
+ if (/^(git|gh|grep|rg|find|cat|head|tail|sed|awk|echo|printf|ls|cd|node|pnpm|npm|yarn|tsx|powershell)$/i.test(firstToken)) {
+ process.exit(0);
+ }
+
+ // Only fire on codex dispatches that start a NEW conversation.
+ // `codex exec resume ` and CLI subcommands (login, mcp, etc.)
+ // are allowed in foreground because they're short-lived control
+ // operations, not work dispatches.
+ const isFreshDispatch = /\bcodex\s+(exec|review)\b/.test(command) &&
+ !/\bcodex\s+exec\s+resume\b/.test(command) &&
+ !/\bcodex\s+(login|logout|mcp|plugin|app|cloud|features|completion|update|sandbox|debug|apply|fork|help)\b/.test(command);
+
+ if (!isFreshDispatch) process.exit(0);
+
+ // The Bash tool flags background via tool_input.run_in_background.
+ // Some harness versions pass it as boolean true; some pass a string
+ // "true". Accept both. Anything else = foreground = block.
+ const bg = hookData?.tool_input?.run_in_background;
+ if (bg === true || bg === "true") process.exit(0);
+
+ const msg =
+ `[enforce-codex-background] BLOCKED — codex dispatch must carry run_in_background: true.\n\n` +
+ `Foreground Codex dispatches block the orchestrator for minutes while Codex churns.\n` +
+ `The rule lives at .claude/codex-delegation.md:7 — but plain-text rules lose to active\n` +
+ `enforcement, so this hook now enforces it.\n\n` +
+ `Fix: re-issue the SAME Bash command with run_in_background: true. The harness will\n` +
+ `notify you when Codex completes; in the meantime, do other work.\n\n` +
+ `If you genuinely need the result inline (you don't — there is almost always other\n` +
+ `work to do), dispatch with Monitor watching the session JSONL — never bypass this hook.\n\n` +
+ `Triggered by command:\n ${command.slice(0, 200)}${command.length > 200 ? '…' : ''}`;
+
+ process.stderr.write(msg + "\n");
+ process.exit(2);
+} catch {
+ process.exit(0);
+}
diff --git a/.claude/hooks/enforce-copy-review-before-commit.mjs b/.claude/hooks/enforce-copy-review-before-commit.mjs
new file mode 100644
index 000000000..914392d2d
--- /dev/null
+++ b/.claude/hooks/enforce-copy-review-before-commit.mjs
@@ -0,0 +1,177 @@
+#!/usr/bin/env node
+// enforce-copy-review-before-commit.mjs
+//
+// PreToolUse hook on Bash: when the command is `git commit` AND the
+// staged diff touches user-facing copy files, REQUIRE the current
+// turn to have shown Mike a before/after of the copy AND called
+// AskUserQuestion with predicted complaints + freeform "Other".
+//
+// 2026-05-16 trigger: Mike, verbatim — *"And you're supposed to like
+// fucking tell me what the previous text was and what you changed it
+// to before you fucking commit and ask me if that's OK. If you'd
+// like give me like a multiple choice questions with buttons that I
+// can click if I like with the things that you think I might wanna
+// change and then a freeform one. Can you add that to your protocol
+// too anytime you change copy? And force yourself to do it."* — after
+// I attempted to commit a /plaintiffs rewrite without showing him
+// the before/after or asking.
+//
+// Why: copy changes are taste calls. Mike is the human gradient
+// signal. I cannot judge whether the new wording lands without him.
+// Committing without a before/after + AskUserQuestion ships untested
+// taste into the campaign critical path.
+//
+// What counts as user-facing copy (must trigger this hook):
+// packages/web/src/app/**/*.tsx
+// packages/web/src/app/**/*.md (auto-generated snapshots from the .tsx)
+// packages/web/src/components/**/*.tsx
+// packages/web/src/lib/routes.ts
+// packages/web/src/lib/messaging.ts
+// packages/web/src/lib/email/**
+// packages/web/emails/**
+// packages/web/src/components/people/*ShareCard*
+// packages/web/src/components/people/*SignatureBox*
+//
+// What the hook checks (best-effort, transcript-based):
+// 1. Staged diff includes at least one copy file (above patterns).
+// 2. Current-turn assistant text shows a before/after diff display
+// (a "BEFORE:"/"AFTER:" or "Old:"/"New:" or backtick-delimited
+// old/new blocks).
+// 3. AskUserQuestion was called in the current turn.
+//
+// If 1 fires but 2 or 3 missing → BLOCK with corrective template.
+//
+// Related memory:
+// - [[feedback_one_at_a_time_review_loop_with_predicted_fixes]]
+// - [[feedback_promote_violated_text_rules_to_hooks]]
+// - [[feedback_verify_ui_fix_before_commit]]
+
+import { existsSync, readFileSync } from "node:fs";
+import { execSync } from "node:child_process";
+
+const COPY_PATTERNS = [
+ /^packages\/web\/src\/app\/.*\.(tsx|md)$/,
+ /^packages\/web\/src\/components\/.*\.tsx$/,
+ /^packages\/web\/src\/lib\/routes\.ts$/,
+ /^packages\/web\/src\/lib\/messaging\.ts$/,
+ /^packages\/web\/src\/lib\/email\//,
+ /^packages\/web\/emails\//,
+];
+
+try {
+ const raw = readFileSync(0, "utf-8");
+ if (!raw || !raw.trim()) process.exit(0);
+
+ const hookData = JSON.parse(raw);
+ if (hookData?.tool_name !== "Bash") process.exit(0);
+
+ const command = String(hookData?.tool_input?.command ?? "");
+ if (!command) process.exit(0);
+
+ // Match `git commit` invocations only. Allow `git commit-tree`, etc.
+ // to pass through. `git -C foo commit` also matches.
+ if (!/\bgit\s+(?:-[CcS]\s+\S+\s+)*commit\b/.test(command)) process.exit(0);
+
+ // Read staged diff name-only via git.
+ let stagedFiles = [];
+ try {
+ const out = execSync("git diff --cached --name-only", {
+ encoding: "utf-8",
+ cwd: hookData?.cwd ?? process.cwd(),
+ stdio: ["ignore", "pipe", "ignore"],
+ });
+ stagedFiles = out.split(/\r?\n/).filter(Boolean);
+ } catch {
+ process.exit(0);
+ }
+ if (stagedFiles.length === 0) process.exit(0);
+
+ const copyFiles = stagedFiles.filter((f) =>
+ COPY_PATTERNS.some((re) => re.test(f.replace(/\\/g, "/"))),
+ );
+ if (copyFiles.length === 0) process.exit(0);
+
+ // Read the current-turn assistant text from the transcript.
+ const transcriptPath =
+ hookData?.transcript_path ?? hookData?.transcriptPath;
+ let chatText = "";
+ let askedThisTurn = false;
+ if (typeof transcriptPath === "string" && existsSync(transcriptPath)) {
+ const lines = readFileSync(transcriptPath, "utf-8").split(/\r?\n/);
+ const entries = [];
+ for (const line of lines) {
+ if (!line.trim()) continue;
+ try {
+ entries.push(JSON.parse(line));
+ } catch {
+ // ignore malformed
+ }
+ }
+
+ let lastHumanIndex = -1;
+ for (let i = 0; i < entries.length; i += 1) {
+ const e = entries[i];
+ if (e?.type !== "user") continue;
+ if (e?.sourceToolAssistantUUID) continue;
+ const content = e?.message?.content;
+ if (Array.isArray(content)) {
+ if (content.every((part) => part?.type === "tool_result")) continue;
+ } else if (typeof content !== "string") {
+ continue;
+ }
+ lastHumanIndex = i;
+ }
+
+ for (let i = lastHumanIndex + 1; i < entries.length; i += 1) {
+ const e = entries[i];
+ if (e?.type !== "assistant") continue;
+ const content = e?.message?.content;
+ if (!Array.isArray(content)) continue;
+ for (const part of content) {
+ if (part?.type === "text" && typeof part.text === "string") {
+ chatText += part.text + "\n";
+ }
+ if (part?.type === "tool_use" && part?.name === "AskUserQuestion") {
+ askedThisTurn = true;
+ }
+ }
+ }
+ }
+
+ // Heuristic: did I show a before/after?
+ const ct = chatText.toLowerCase();
+ const showsBeforeAfter =
+ (/\bbefore\b[\s\S]{0,400}\bafter\b/.test(ct)) ||
+ (/\bold\b[:\s][\s\S]{0,400}\bnew\b/.test(ct)) ||
+ (/\bwas\b[\s\S]{0,200}\bnow\b/.test(ct));
+
+ if (showsBeforeAfter && askedThisTurn) process.exit(0);
+
+ const fileList = copyFiles.map((f) => ` - ${f}`).join("\n");
+ const msg =
+ `[enforce-copy-review-before-commit] BLOCKED — copy commit without before/after review.\n\n` +
+ `Staged copy files:\n${fileList}\n\n` +
+ `Missing in current turn:\n` +
+ (showsBeforeAfter ? `` : ` - Before/After diff display (Mike needs to see OLD text + NEW text in chat)\n`) +
+ (askedThisTurn ? `` : ` - AskUserQuestion with predicted complaints + freeform Other\n`) +
+ `\n` +
+ `Required template before re-attempting commit:\n\n` +
+ ` **BEFORE:** \n\n` +
+ ` **AFTER:** \n\n` +
+ ` Predicted complaints:\n` +
+ ` 1. **A: Looks good, ship it**\n` +
+ ` 2. **B: **\n` +
+ ` 3. **C: **\n` +
+ ` 4. **D: **\n` +
+ ` (Other: freeform — Mike types his own complaint)\n\n` +
+ ` [AskUserQuestion call here]\n\n` +
+ `Why: copy changes are taste calls; Mike is the human gradient signal.\n` +
+ `Rule lives at: feedback_show_before_after_and_ask_before_copy_commit.md\n` +
+ `Doc: .claude/codex-delegation.md (delegation rules)\n` +
+ `Related hook: review-loop-gate.mjs (post-deploy review queue)`;
+
+ process.stderr.write(msg + "\n");
+ process.exit(2);
+} catch {
+ process.exit(0);
+}
diff --git a/.claude/hooks/enforce-theory-of-mind-on-copy-edit.mjs b/.claude/hooks/enforce-theory-of-mind-on-copy-edit.mjs
new file mode 100644
index 000000000..1b13c0b5b
--- /dev/null
+++ b/.claude/hooks/enforce-theory-of-mind-on-copy-edit.mjs
@@ -0,0 +1,211 @@
+#!/usr/bin/env node
+// enforce-theory-of-mind-on-copy-edit.mjs
+//
+// PreToolUse hook on Edit / Write / MultiEdit: when the file path
+// matches user-facing copy (route pages, components, lib/routes,
+// lib/messaging, email templates), REQUIRE the current-turn chat
+// text to contain a theory-of-mind block — explicit Audience, Goal,
+// and Theory-of-Mind reader simulation — BEFORE the edit lands.
+//
+// 2026-05-16 trigger: Mike, verbatim — *"Like it would be nice if
+// you like simulated a theory of mind of the viewer, the reader of
+// everything that we write instead of. It seems like you're not
+// doing that, am I? And like, figure out what we want them to do.
+// On the page who it is and what words would make them do what we
+// want them to do? It seems like you're just like throwing a bunch
+// of words on the pages. That are similar to whatever the fuck I
+// said. Like is it possible to force you to do this and would it
+// help if we did?"*
+//
+// Sister hook: enforce-audience-and-goal-on-ui-dispatch.mjs fires
+// only on Codex dispatches. This hook fires on direct Edit/Write —
+// the gap that lets me ship copy without simulating the reader.
+//
+// What counts as user-facing copy (triggers this hook):
+// packages/web/src/app/**/page.tsx
+// packages/web/src/app/**/page.logged-out.md (auto-generated; skip)
+// packages/web/src/components/**/*.tsx (component-level copy)
+// packages/web/src/lib/routes.ts
+// packages/web/src/lib/messaging.ts
+// packages/web/src/lib/email/**
+// packages/web/emails/**
+//
+// What the hook checks (current-turn assistant text):
+// 1. An Audience phrase — concrete persona naming (audience, viewer,
+// reader, persona, "who", "grieving family", "org leaders",
+// "donors", "voters", "plaintiffs", "signers", "endorsers",
+// "politicians", "the user is", "people who")
+// 2. A Goal phrase — concrete action (goal, want them to, action,
+// conversion, primary action, click, sign, register, endorse,
+// donate, vote, share, scroll to)
+// 3. A reader-simulation phrase — theory of mind, blocker, what
+// stops, what makes them act, what they fear, what converts
+//
+// All three must be present in the assistant text written between
+// the last human user message and the current Edit/Write call.
+// Missing any => BLOCK with corrective template.
+//
+// Bypass conditions:
+// - File is page.logged-out.md (auto-generated snapshot)
+// - File matches packages/web/src/lib/email/*.ts AND change is
+// mechanical (handled by separate enforce hook)
+// - Edit is a trivial mechanical change (typo, import reorder,
+// formatting) — but we can't detect that here, so we don't
+// bypass — the hook is best-effort, not a tribunal.
+
+import { existsSync, readFileSync } from "node:fs";
+
+const COPY_PATTERNS = [
+ /^packages\/web\/src\/app\/.*\/page\.tsx$/,
+ /^packages\/web\/src\/components\/.*\.tsx$/,
+ /^packages\/web\/src\/lib\/routes\.ts$/,
+ /^packages\/web\/src\/lib\/messaging\.ts$/,
+ /^packages\/web\/src\/lib\/email\//,
+ /^packages\/web\/emails\//,
+];
+
+// Skip auto-generated snapshots and tests.
+const SKIP_PATTERNS = [
+ /\.logged-out\.md$/,
+ /\.email\.md$/,
+ /\.test\.tsx?$/,
+ /\.spec\.tsx?$/,
+];
+
+try {
+ const raw = readFileSync(0, "utf-8");
+ if (!raw || !raw.trim()) process.exit(0);
+
+ const hookData = JSON.parse(raw);
+ const tool = hookData?.tool_name;
+ if (tool !== "Edit" && tool !== "Write" && tool !== "MultiEdit") {
+ process.exit(0);
+ }
+
+ const filePath = String(hookData?.tool_input?.file_path ?? "");
+ if (!filePath) process.exit(0);
+
+ const normalized = filePath.replace(/\\/g, "/");
+ if (SKIP_PATTERNS.some((re) => re.test(normalized))) process.exit(0);
+
+ const matchesCopy = COPY_PATTERNS.some((re) =>
+ re.test(normalized) ||
+ re.test(normalized.replace(/^.*\/packages\//, "packages/")),
+ );
+ if (!matchesCopy) process.exit(0);
+
+ // Read current-turn assistant text from transcript.
+ const transcriptPath =
+ hookData?.transcript_path ?? hookData?.transcriptPath;
+ if (typeof transcriptPath !== "string" || !existsSync(transcriptPath)) {
+ process.exit(0); // can't verify, fail open
+ }
+
+ const lines = readFileSync(transcriptPath, "utf-8").split(/\r?\n/);
+ const entries = [];
+ for (const line of lines) {
+ if (!line.trim()) continue;
+ try {
+ entries.push(JSON.parse(line));
+ } catch {
+ // ignore malformed
+ }
+ }
+
+ let lastHumanIndex = -1;
+ for (let i = 0; i < entries.length; i += 1) {
+ const e = entries[i];
+ if (e?.type !== "user") continue;
+ if (e?.sourceToolAssistantUUID) continue;
+ const content = e?.message?.content;
+ if (Array.isArray(content)) {
+ if (content.every((part) => part?.type === "tool_result")) continue;
+ } else if (typeof content !== "string") {
+ continue;
+ }
+ lastHumanIndex = i;
+ }
+
+ let chatText = "";
+ for (let i = lastHumanIndex + 1; i < entries.length; i += 1) {
+ const e = entries[i];
+ if (e?.type !== "assistant") continue;
+ const content = e?.message?.content;
+ if (!Array.isArray(content)) continue;
+ for (const part of content) {
+ if (part?.type === "text" && typeof part.text === "string") {
+ chatText += part.text + "\n";
+ }
+ }
+ }
+
+ const ct = chatText.toLowerCase();
+
+ // Transcript flush is unreliable at PreToolUse time — if we have
+ // <100 chars of text, exit advisory not blocking.
+ if (ct.length < 100) {
+ process.stderr.write(
+ `[enforce-theory-of-mind-on-copy-edit] ADVISORY — chat text too short to verify; if I haven't simulated the reader for ${filePath}, do so before the next edit.\n`,
+ );
+ process.exit(0);
+ }
+
+ // Audience: concrete persona naming
+ const audienceHit =
+ /\baudience\b/.test(ct) ||
+ /\bpersona\b/.test(ct) ||
+ /\b(who|whom)\b.*\b(is|are|reads?|visits?|views?|opens?)\b/.test(ct) ||
+ /\bviewer\b/.test(ct) ||
+ /\breader\b/.test(ct) ||
+ /\bgrieving (family|family member|relative)\b/.test(ct) ||
+ /\b(org leaders?|donors?|voters?|plaintiffs?|signers?|endorsers?|politicians?|presidents?|signatories?|recruiters?|coalition partners?|public figures?)\b/.test(ct) ||
+ /\bthe (user|visitor) (is|are|wants|sees|reads|views|arrives)\b/.test(ct) ||
+ /\bpeople who\b/.test(ct);
+
+ // Goal: concrete desired action
+ const goalHit =
+ /\bgoal\b/.test(ct) ||
+ /\bwe want\b/.test(ct) ||
+ /\bwant them to\b/.test(ct) ||
+ /\bprimary action\b/.test(ct) ||
+ /\bthe conversion\b/.test(ct) ||
+ /\bcta\b/.test(ct) ||
+ /\bnext step\b/.test(ct) ||
+ /\b(click|sign|register|endorse|donate|vote|share|subscribe|scroll to|fill (in|out)|submit|tap)\s+(the|a|on|to|down|→)?/i.test(ct);
+
+ // Theory of mind: explicit reader simulation
+ const tomHit =
+ /\btheory of mind\b/.test(ct) ||
+ /\bblocker(s)?\b/.test(ct) ||
+ /\bwhat (stops|blocks|prevents|makes|gets) them\b/.test(ct) ||
+ /\bthey (fear|worry|wonder|hesitate|think|expect)\b/.test(ct) ||
+ /\bconvert(s|ed|ing|er)?\b/.test(ct) ||
+ /\b(scam|credible|credibility|trustworth)/.test(ct) ||
+ /\b(what they want|what converts them|what pushes them)\b/.test(ct);
+
+ if (audienceHit && goalHit && tomHit) process.exit(0);
+
+ const missing = [];
+ if (!audienceHit) missing.push("Audience (concrete persona)");
+ if (!goalHit) missing.push("Goal (concrete action)");
+ if (!tomHit) missing.push("Theory of mind (reader's blockers + lever)");
+
+ const msg =
+ `[enforce-theory-of-mind-on-copy-edit] BLOCKED — copy edit without reader simulation.\n\n` +
+ `File: ${filePath}\n\n` +
+ `Missing in current turn's chat text:\n${missing.map((m) => ` - ${m}`).join("\n")}\n\n` +
+ `Required template before re-attempting Edit/Write:\n\n` +
+ ` **Audience:** \n` +
+ ` **Goal:** \n` +
+ ` **Theory of mind:** \n\n` +
+ `Then write the edit. The simulation is the whole point — without it, copy is\n` +
+ `"throwing a bunch of words" (Mike's words) at strangers.\n\n` +
+ `Sister hooks: enforce-audience-and-goal-on-ui-dispatch.mjs (Codex dispatches),\n` +
+ `enforce-copy-review-before-commit.mjs (before/after + AskUserQuestion).\n` +
+ `Rule lives at: feedback_simulate_reader_theory_of_mind_before_copy_edit.md`;
+
+ process.stderr.write(msg + "\n");
+ process.exit(2);
+} catch {
+ process.exit(0);
+}
diff --git a/.claude/settings.json b/.claude/settings.json
index 3d80878d1..993997c83 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -23,6 +23,21 @@
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/block-snapshot-handedit.mjs\"",
"timeout": 3000
+ },
+ {
+ "type": "command",
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-theory-of-mind-on-copy-edit.mjs\"",
+ "timeout": 5000
+ }
+ ]
+ },
+ {
+ "matcher": "MultiEdit",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-theory-of-mind-on-copy-edit.mjs\"",
+ "timeout": 5000
}
]
},
@@ -59,6 +74,11 @@
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/pre-commit-checklist.mjs\"",
"timeout": 10000
},
+ {
+ "type": "command",
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-copy-review-before-commit.mjs\"",
+ "timeout": 5000
+ },
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/codex-dispatch-blather.mjs\"",
@@ -69,6 +89,11 @@
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-codex-protocol.mjs\"",
"timeout": 3000
},
+ {
+ "type": "command",
+ "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-codex-background.mjs\"",
+ "timeout": 3000
+ },
{
"type": "command",
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-manual-search-in-copy-dispatch.mjs\"",
From 74f1115eea541a800712c226a28bcbc48df56263 Mon Sep 17 00:00:00 2001
From: "Mike P. Sinn"
Date: Sat, 16 May 2026 17:33:00 -0500
Subject: [PATCH 03/22] /treaty: mobile slider fixes + 'health and wealth'
gloss (surface-local)
Mobile UX on the /treaty slider screen:
- Add px-4 side padding (was edge-to-edge on mobile via px-0)
- Compress vertical rhythm: h1 text-3xl -> text-2xl, allocation
text-5xl mb-2 -> text-4xl mb-1, slider chrome pb-10 pt-3 -> pb-3
pt-1, content space-y-3 (was implicit). Submit button now fits in
iPhone viewport after slider drag.
- Fix auto-scroll on slider release: scrollIntoView block: 'nearest'
was too conservative (button could be barely visible and "scrolled").
Switch to block: 'center' with 75ms setTimeout so AnimatePresence
mount completes before the scroll fires.
Slider question copy (surface-local hardcode in messaging.ts,
canonical WelfareClaim unchanged):
- Was: "You pay governments $36.5T a year to promote the general welfare
-- i.e. maximize median healthy life years and median after-tax
inflation-adjusted income. What allocation..."
- Now: "You pay governments $36.5T a year to promote the general welfare
(health and wealth). What allocation..."
- 38 -> 25 words. "Health and wealth" parenthetical gloss replaces 8
nested modifiers. Surface-local: other pages quoting WelfareClaim
unchanged.
E2E test updated: treaty-vote-click.spec.ts now expects scrollIntoView
with block: 'center' after slider release (was previously asserted to
NOT auto-scroll). 6 tests passed with repeat-each=3.
Audience: voter on mobile (iPhone viewport) on /treaty.
Goal: drag the slider, then tap SUBMIT in same viewport.
Theory of mind: dense academic modifiers ("median healthy life years
and median after-tax inflation-adjusted income") forced parsing; the
"health and wealth" gloss removes the bounce. Mobile padding + scroll
fix ensures SUBMIT is reachable.
qa-passed: Codex preflight b3a75l98u (typecheck, unit tests, methodology
links 200, other welfare-claim surfaces unchanged) + bm4xttbzb (e2e
test updated for block:'center', 6 passed repeat-each=3).
Co-Authored-By: Claude Opus 4.7 (1M context)
---
packages/web/e2e/treaty-vote-click.spec.ts | 146 +++++++++++++-----
.../web/src/app/treaty/page.logged-out.md | 2 +-
.../landing/TreatyVoteFlow.test.tsx | 38 +++++
.../src/components/landing/TreatyVoteFlow.tsx | 33 ++--
packages/web/src/lib/messaging.ts | 4 +-
5 files changed, 165 insertions(+), 58 deletions(-)
diff --git a/packages/web/e2e/treaty-vote-click.spec.ts b/packages/web/e2e/treaty-vote-click.spec.ts
index c46f7b46e..a39c7ca64 100644
--- a/packages/web/e2e/treaty-vote-click.spec.ts
+++ b/packages/web/e2e/treaty-vote-click.spec.ts
@@ -1,21 +1,96 @@
-import { expect, type Page, test } from "@playwright/test";
+import { expect, type Locator, type Page, test } from "@playwright/test";
-const VOTE_URL =
- "/vote?ref=mike&invite=Uyn3Wl7O_OGdiVyJ4y0-fAhZ&treatyFlow=v1";
+const VOTE_URL = "/vote?ref=mike&invite=Uyn3Wl7O_OGdiVyJ4y0-fAhZ&treatyFlow=v1";
const VOTE_INVITE_CODE = "mike";
const VOTE_INVITE_TOKEN = "Uyn3Wl7O_OGdiVyJ4y0-fAhZ";
+type ScrollIntoViewCall = {
+ behavior: ScrollBehavior | null;
+ block: ScrollLogicalPosition | null;
+ inline: ScrollLogicalPosition | null;
+ text: string;
+};
+
+type TreatyScrollWindow = Window & {
+ __treatyScrollIntoViewCalls?: ScrollIntoViewCall[];
+ __treatyScrollIntoViewRecorderInstalled?: boolean;
+};
+
+async function installScrollIntoViewRecorder(page: Page): Promise {
+ await page.addInitScript(() => {
+ const win = window as TreatyScrollWindow;
+ if (win.__treatyScrollIntoViewRecorderInstalled) return;
+
+ win.__treatyScrollIntoViewRecorderInstalled = true;
+ win.__treatyScrollIntoViewCalls = [];
+
+ const originalScrollIntoView = Element.prototype.scrollIntoView;
+ Element.prototype.scrollIntoView = function scrollIntoViewRecorder(
+ arg?: boolean | ScrollIntoViewOptions,
+ ) {
+ const options = typeof arg === "object" && arg !== null ? arg : {};
+ win.__treatyScrollIntoViewCalls?.push({
+ behavior: options.behavior ?? null,
+ block: options.block ?? null,
+ inline: options.inline ?? null,
+ text: this.textContent?.trim() ?? "",
+ });
+
+ return originalScrollIntoView.call(this, arg);
+ };
+ });
+}
+
+async function clearScrollIntoViewCalls(page: Page): Promise {
+ await page.evaluate(() => {
+ const win = window as TreatyScrollWindow;
+ win.__treatyScrollIntoViewCalls = [];
+ });
+}
+
+async function expectSubmitAutoScrollAfterRelease(
+ page: Page,
+ submit: Locator,
+): Promise {
+ await expect
+ .poll(
+ async () =>
+ page.evaluate(() => {
+ const win = window as TreatyScrollWindow;
+ return (win.__treatyScrollIntoViewCalls ?? []).filter(
+ (call) =>
+ call.text.includes("SUBMIT") &&
+ call.behavior === "smooth" &&
+ call.block === "center",
+ ).length;
+ }),
+ {
+ message:
+ "releasing the allocation slider should call smooth center scroll on SUBMIT",
+ timeout: 3_000,
+ },
+ )
+ .toBeGreaterThan(0);
+
+ await expect(submit).toBeInViewport({ timeout: 3_000 });
+}
+
async function completeSliderAndVote(page: Page): Promise {
const voteSection = page.locator("#vote");
const slider = voteSection.locator('input[type="range"]');
await expect(slider).toBeVisible({ timeout: 15_000 });
const submit = voteSection.locator("button:has-text('SUBMIT')");
+ const dragAttempts: string[] = [];
+ let submitRevealed = false;
+
+ await clearScrollIntoViewCalls(page);
for (const targetRatio of [0.3, 0.25, 0.35]) {
const box = await slider.boundingBox();
expect(box, "slider track should have geometry").not.toBeNull();
if (!box) return;
+ const valueBeforeDrag = await slider.inputValue();
const y = box.y + box.height / 2;
const targetX = box.x + box.width * targetRatio;
const startX = box.x + box.width / 2;
@@ -25,50 +100,45 @@ async function completeSliderAndVote(page: Page): Promise {
await page.mouse.move(targetX, y, { steps: 12 });
await page.mouse.up();
- if (await submit.isVisible({ timeout: 1_500 }).catch(() => false)) {
+ const valueAfterDrag = await slider.inputValue();
+ const submitVisible = await submit
+ .isVisible({ timeout: 1_500 })
+ .catch(() => false);
+ dragAttempts.push(
+ `target=${targetRatio}; slider=${valueBeforeDrag}->${valueAfterDrag}; submit=${
+ submitVisible ? "visible" : "hidden"
+ }`,
+ );
+
+ if (submitVisible) {
+ await expectSubmitAutoScrollAfterRelease(page, submit);
+ submitRevealed = true;
break;
}
}
+ expect(
+ submitRevealed,
+ `SUBMIT should reveal after a real slider drag. Attempts: ${dragAttempts.join(
+ " | ",
+ )}`,
+ ).toBe(true);
await expect(submit).toBeVisible({ timeout: 10_000 });
- const scrollYBeforeSubmit = await page.evaluate(() => window.scrollY);
await submit.click();
const yesButton = voteSection.locator("button:has-text('YES')");
await expect(yesButton).toBeVisible({ timeout: 10_000 });
- const maxSubmitScrollDelta = await page.evaluate(
- (scrollYBeforeSubmit) =>
- new Promise((resolve) => {
- const startedAt = performance.now();
- let maxDelta = 0;
-
- const sample = () => {
- maxDelta = Math.max(
- maxDelta,
- Math.abs(window.scrollY - scrollYBeforeSubmit),
- );
-
- if (performance.now() - startedAt >= 700) {
- resolve(maxDelta);
- return;
- }
-
- requestAnimationFrame(sample);
- };
-
- requestAnimationFrame(sample);
- }),
- scrollYBeforeSubmit,
- );
- expect(
- maxSubmitScrollDelta,
- "submitting the allocation slider should not auto-scroll the page",
- ).toBeLessThan(80);
await yesButton.click();
}
test.describe("treaty vote yes-click regression", () => {
- test("anonymous visitor reaches post-vote save flow after YES", async ({ page }) => {
+ test.beforeEach(async ({ page }) => {
+ await installScrollIntoViewRecorder(page);
+ });
+
+ test("anonymous visitor reaches post-vote save flow after YES", async ({
+ page,
+ }) => {
const response = await page.goto(VOTE_URL, {
waitUntil: "domcontentloaded",
});
@@ -93,7 +163,9 @@ test.describe("treaty vote yes-click regression", () => {
const postVoteRedirect = page.getByTestId("treaty-post-vote-redirect");
await expect(postVoteRedirect).toBeVisible({ timeout: 15_000 });
await expect(
- postVoteRedirect.getByRole("button", { name: /(Save|Verify) with Google/i }),
+ postVoteRedirect.getByRole("button", {
+ name: /(Save|Verify) with Google/i,
+ }),
).toBeVisible({ timeout: 10_000 });
});
@@ -139,7 +211,9 @@ test.describe("treaty vote yes-click regression", () => {
const postVoteRedirect = page.getByTestId("treaty-post-vote-redirect");
await expect(postVoteRedirect).toBeVisible({ timeout: 15_000 });
await expect(
- postVoteRedirect.getByRole("button", { name: /(Save|Verify) with Google/i }),
+ postVoteRedirect.getByRole("button", {
+ name: /(Save|Verify) with Google/i,
+ }),
).toBeVisible({ timeout: 10_000 });
});
});
diff --git a/packages/web/src/app/treaty/page.logged-out.md b/packages/web/src/app/treaty/page.logged-out.md
index d8724397d..ae8c8b83e 100644
--- a/packages/web/src/app/treaty/page.logged-out.md
+++ b/packages/web/src/app/treaty/page.logged-out.md
@@ -50,6 +50,6 @@
- Article VIII: This treaty supersedes all conflicting domestic law. Including the subsection your legislature added at 2 a.m. last session specifically to make sure this couldn't happen.
- Article IX: This Treaty enters into force upon signature by two states. War has killed humans for as long as there have been humans to kill. Disease has been killing them longer. Its founding signatories will be responsible for the largest reduction in human suffering and the largest increase in human prosperity in the history of planet Earth.
- IN WITNESS WHEREOF, the undersigned, being of sound mind (debatable) and tired of watching their loved ones die of preventable diseases, have executed this Treaty.
-- Signed this day, May 15, 2026, in the year of our ongoing confusion.
+- Signed this day, May 16, 2026, in the year of our ongoing confusion.
- SIGN
- Display my name publicly on the signer list and leaderboards (recommended).
diff --git a/packages/web/src/components/landing/TreatyVoteFlow.test.tsx b/packages/web/src/components/landing/TreatyVoteFlow.test.tsx
index 83a162732..b8f73900a 100644
--- a/packages/web/src/components/landing/TreatyVoteFlow.test.tsx
+++ b/packages/web/src/components/landing/TreatyVoteFlow.test.tsx
@@ -172,4 +172,42 @@ describe("TreatyVoteFlow", () => {
expect(scrollIntoView).toHaveBeenCalledTimes(2);
});
+
+ it("centers the submit button after slider release", async () => {
+ await act(async () => {
+ root.render(
+ ,
+ );
+ });
+
+ const slider = container.querySelector(
+ 'input[type="range"]',
+ );
+ expect(slider).not.toBeNull();
+
+ await act(async () => {
+ slider!.value = "70";
+ Simulate.change(slider!);
+ });
+
+ scrollIntoView.mockClear();
+ await act(async () => {
+ Simulate.pointerUp(slider!);
+ });
+
+ expect(scrollIntoView).not.toHaveBeenCalled();
+
+ await act(async () => {
+ vi.advanceTimersByTime(75);
+ });
+
+ expect(scrollIntoView).toHaveBeenCalledWith({
+ behavior: "smooth",
+ block: "center",
+ });
+ expect(scrollIntoView).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/packages/web/src/components/landing/TreatyVoteFlow.tsx b/packages/web/src/components/landing/TreatyVoteFlow.tsx
index 37e05ea4d..aa74a1d88 100644
--- a/packages/web/src/components/landing/TreatyVoteFlow.tsx
+++ b/packages/web/src/components/landing/TreatyVoteFlow.tsx
@@ -173,31 +173,26 @@ export function TreatyVoteFlow({
const animationFrameRef = useRef(null);
const postVoteRedirectStartedRef = useRef(false);
const initialVoteShellClassName = compactInitialScreen
- ? "min-h-0 overflow-visible px-0 py-0 sm:px-0 sm:py-0"
+ ? "min-h-0 overflow-visible px-4 py-0 sm:px-8 sm:py-0"
: "py-3 sm:py-10";
// Cut mobile vertical spacing so the submit button (which appears after
// first slider drag) fits in the viewport. TreatyFlowShell defaults to
// `space-y-10 py-10` between children; on mobile that adds ~120px of
// gap stacked across headline / paragraph / allocation / submit, which
- // pushes the submit below the fold. Desktop spacing unchanged via sm:.
+ // pushes the submit below the fold.
const initialVoteContentClassName = compactInitialScreen
- ? "max-w-4xl flex-none justify-start py-0 sm:py-0"
+ ? "max-w-4xl flex-none justify-start space-y-3 py-2 sm:space-y-6 sm:py-0"
: "max-w-4xl space-y-5 py-4 sm:space-y-10 sm:py-12";
- // After the user releases the slider, scroll the just-revealed submit
- // button into view (block: 'nearest' — scrolls only the minimum to make
- // it visible). Triggered on pointerup so we don't disrupt the active
- // drag interaction; only fires once userHasDragged is true (submit
- // button is mounted).
+ // Give AnimatePresence a moment to mount the submit button before scrolling.
const handleSliderRelease = () => {
if (typeof window === "undefined") return;
- // requestAnimationFrame defers until the AnimatePresence mount completes.
- window.requestAnimationFrame(() => {
+ window.setTimeout(() => {
submitButtonRef.current?.scrollIntoView({
behavior: "smooth",
- block: "nearest",
+ block: "center",
});
- });
+ }, 75);
};
useEffect(() => {
@@ -758,22 +753,22 @@ export function TreatyVoteFlow({
className={initialVoteShellClassName}
contentClassName={initialVoteContentClassName}
>
-
@@ -781,7 +776,7 @@ export function TreatyVoteFlow({
-
+
{clinicalTrialsAllocation}%
@@ -791,7 +786,7 @@ export function TreatyVoteFlow({
{/* Slider with Animation */}
-
+
{showAnimation && !userHasDragged && (
<>
diff --git a/packages/web/src/lib/messaging.ts b/packages/web/src/lib/messaging.ts
index 3e939230f..c8944c0a0 100644
--- a/packages/web/src/lib/messaging.ts
+++ b/packages/web/src/lib/messaging.ts
@@ -13,7 +13,7 @@ import {
HUMANITY_MANAGEMENT,
ORGANIZATION_ACTIVATION_TASK_TITLE,
} from "@optimitron/data/campaign";
-import { WELFARE_CLAIM_TEXT } from "@/components/shared/WelfareClaim.core";
+import { WELFARE_CLAIM_AMOUNT_TEXT } from "@/components/shared/WelfareClaim.core";
/** Point name — single source of truth. Change here to rename everywhere. */
export const POINT_NAME = "VOTE" as const;
@@ -278,7 +278,7 @@ export const VOTE_SECTION = {
// Humanity v. Government: governments are paid ~$36T/yr to promote
// the general welfare and underdeliver. Voters' welfare-findings
// become evidence in the case; preferences alone would not.
- sliderPrompt: `${WELFARE_CLAIM_TEXT} What allocation between military spending and clinical trials would best fulfill that duty?`,
+ sliderPrompt: `You pay governments ${WELFARE_CLAIM_AMOUNT_TEXT} a year to promote the general welfare (health and wealth). What allocation between military spending and clinical trials would best fulfill that duty?`,
realityCheck:
"on weapons and military systems for every $1 spent on clinical trials.",
theQuestion:
From a016db399d81f8a87c44e20ba599ad7a010fb5f7 Mon Sep 17 00:00:00 2001
From: "Mike P. Sinn"
Date: Sat, 16 May 2026 17:55:33 -0500
Subject: [PATCH 04/22] llms.txt + AI-search defensibility infra
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Ship agent-readable infrastructure so AI search engines (ChatGPT,
Claude, Gemini, Perplexity) and browsing agents (GPTBot, ClaudeBot,
PerplexityBot, OAI-SearchBot, Claude-SearchBot) can answer the core
War on Disease questions from canonical warondisease.org sources.
CEO subagent on the prioritization plan flagged this as the #1 missing
top-7 item: "llms.txt / agent-readable schema is the 2026 SEO equivalent
and we have zero coverage."
Shipped:
- /llms.txt + /llms-full.txt (route-level structured campaign canon)
- Markdown mirrors: /treaty.md, /court.md, /humanity-v-government.md,
/plaintiffs.md, /faq.md (curated for LLM consumption)
- /faq visible page with FAQPage JSON-LD covering the 4 target questions
(What is the 1% Treaty, Humanity v Government, register a plaintiff,
health and wealth math)
- /api/agent/* endpoints: manifest, campaign-state, signatories,
plaintiffs, parameters (structured JSON for agentic queries)
- Route-level JSON-LD: Legislation on /treaty, VoteAction on /vote,
Claim on /court and /humanity-v-government (schema.org-supported
types only; Petition/CourtCase do not exist as native types)
- Sitemap.xml extended to advertise llms/mirrors/API surfaces
- robots.txt: allow /api/agent/* (was disallowing all /api), keep
/api, auth/, dashboard/, profile/, settings/ blocked
- AI crawler classifier middleware: logs requests from known AI bots
with provider/purpose/path classification to enable AI-search-referral
measurement without PII
Plan: .claude/plans/llms-txt-and-ai-search-defensibility.md
Audience: AI search engines + browsing agents (GPTBot, ClaudeBot,
PerplexityBot, Google-Extended, OAI-SearchBot, Claude-SearchBot,
Perplexity-User) + future llms.txt-consuming developer agents.
Goal: be the authoritative source for War on Disease facts in
AI search results, not a wiki/news/social rephrasing.
Theory of mind: AI agents won't crawl conversion-optimized HTML;
they want curated facts, structured data, and stable URLs. Without
this, future "what is the 1% Treaty" queries answer from stale
news clips or social posts.
qa-passed: Codex bhqqab5z3 — tests pass for agent-readable content,
schema types, sitemap, robots, crawler detection; typecheck:fast
clean; live checks on /llms.txt + /api/agent/* return 200 with
correct content-types; JSON-LD confirmed on /treaty /vote /court
/humanity-v-government /faq via page-source check.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../llms-txt-and-ai-search-defensibility.md | 333 ++++++++++++++++++
.../src/app/api/agent/campaign-state/route.ts | 18 +
.../web/src/app/api/agent/manifest/route.ts | 16 +
.../web/src/app/api/agent/parameters/route.ts | 16 +
.../web/src/app/api/agent/plaintiffs/route.ts | 16 +
.../src/app/api/agent/signatories/route.ts | 16 +
packages/web/src/app/court.md/route.ts | 27 ++
packages/web/src/app/court/page.tsx | 3 +
packages/web/src/app/faq.md/route.ts | 18 +
packages/web/src/app/faq/page.logged-out.md | 24 ++
packages/web/src/app/faq/page.tsx | 39 ++
.../src/app/humanity-v-government.md/route.ts | 18 +
.../src/app/humanity-v-government/page.tsx | 9 +
packages/web/src/app/llms-full.txt/route.ts | 18 +
packages/web/src/app/llms.txt/route.ts | 18 +
packages/web/src/app/page.logged-out.md | 2 +-
packages/web/src/app/plaintiffs.md/route.ts | 18 +
.../web/src/app/survey/page.logged-out.md | 2 +-
packages/web/src/app/treaty.md/route.ts | 27 ++
packages/web/src/app/treaty/page.tsx | 5 +
packages/web/src/app/vote/page.logged-out.md | 2 +-
packages/web/src/app/vote/page.tsx | 7 +-
.../web/src/components/site/JsonLdScript.tsx | 16 +
.../components/site/SiteStructuredData.tsx | 10 +-
.../src/lib/__tests__/agent-readable.test.ts | 103 ++++++
.../__tests__/ai-crawler-detection.test.ts | 50 +++
.../campaign-structured-data.test.ts | 96 +++++
.../web/src/lib/__tests__/site-assets.test.ts | 31 +-
.../src/lib/__tests__/site-sitemap.test.ts | 15 +
.../lib/agent-readable/agent-api.server.ts | 239 +++++++++++++
.../src/lib/agent-readable/agent-sitemap.ts | 39 ++
.../agent-readable/ai-crawler-detection.ts | 81 +++++
.../src/lib/agent-readable/campaign-canon.ts | 159 +++++++++
.../web/src/lib/agent-readable/llms-text.ts | 91 +++++
.../lib/agent-readable/markdown-mirrors.ts | 150 ++++++++
.../web/src/lib/campaign-structured-data.ts | 177 ++++++++++
packages/web/src/lib/routes.ts | 17 +
packages/web/src/lib/site-assets.ts | 51 ++-
packages/web/src/lib/site-sitemap.ts | 5 +
packages/web/src/middleware.ts | 74 ++++
40 files changed, 2028 insertions(+), 28 deletions(-)
create mode 100644 .claude/plans/llms-txt-and-ai-search-defensibility.md
create mode 100644 packages/web/src/app/api/agent/campaign-state/route.ts
create mode 100644 packages/web/src/app/api/agent/manifest/route.ts
create mode 100644 packages/web/src/app/api/agent/parameters/route.ts
create mode 100644 packages/web/src/app/api/agent/plaintiffs/route.ts
create mode 100644 packages/web/src/app/api/agent/signatories/route.ts
create mode 100644 packages/web/src/app/court.md/route.ts
create mode 100644 packages/web/src/app/faq.md/route.ts
create mode 100644 packages/web/src/app/faq/page.logged-out.md
create mode 100644 packages/web/src/app/faq/page.tsx
create mode 100644 packages/web/src/app/humanity-v-government.md/route.ts
create mode 100644 packages/web/src/app/llms-full.txt/route.ts
create mode 100644 packages/web/src/app/llms.txt/route.ts
create mode 100644 packages/web/src/app/plaintiffs.md/route.ts
create mode 100644 packages/web/src/app/treaty.md/route.ts
create mode 100644 packages/web/src/components/site/JsonLdScript.tsx
create mode 100644 packages/web/src/lib/__tests__/agent-readable.test.ts
create mode 100644 packages/web/src/lib/__tests__/ai-crawler-detection.test.ts
create mode 100644 packages/web/src/lib/__tests__/campaign-structured-data.test.ts
create mode 100644 packages/web/src/lib/agent-readable/agent-api.server.ts
create mode 100644 packages/web/src/lib/agent-readable/agent-sitemap.ts
create mode 100644 packages/web/src/lib/agent-readable/ai-crawler-detection.ts
create mode 100644 packages/web/src/lib/agent-readable/campaign-canon.ts
create mode 100644 packages/web/src/lib/agent-readable/llms-text.ts
create mode 100644 packages/web/src/lib/agent-readable/markdown-mirrors.ts
create mode 100644 packages/web/src/lib/campaign-structured-data.ts
diff --git a/.claude/plans/llms-txt-and-ai-search-defensibility.md b/.claude/plans/llms-txt-and-ai-search-defensibility.md
new file mode 100644
index 000000000..1fb2a7927
--- /dev/null
+++ b/.claude/plans/llms-txt-and-ai-search-defensibility.md
@@ -0,0 +1,333 @@
+# LLMs.txt and AI Search Defensibility
+
+Slug: `llms-txt-and-ai-search-defensibility`
+Branch: `feature/plaintiffs-variant-1-and-copy-hooks`
+Date: 2026-05-16
+Author: Codex
+Status: APPROVED — Mike said "just do this now and add it to whatever pull request we're on" on 2026-05-16; folding into feature/plaintiffs-variant-1-and-copy-hooks branch
+
+## Mike approved
+
+Mike's verbatim approval (2026-05-16):
+> "just do this now and add it to whatever pull request we're on. it's not. it's not a big deal. don't make a big whole new pull request for trivia. little stuff like this"
+
+Scope confirmed: bundle into current branch alongside /treaty fix. No separate PR.
+
+## Brief
+
+Build agent-readable campaign infrastructure so AI search engines and browsing agents can answer the core War on Disease questions from canonical `warondisease.org` sources instead of guessing from rendered pages, stale snippets, or social copies.
+
+The first target questions are:
+
+- "What is the 1% Treaty?"
+- "What is Humanity v Government?"
+- "How do I register a plaintiff?"
+- "What is the health and wealth math?"
+
+This is a plan only. Do not implement until autoplan critique is complete and Mike explicitly approves the final plan.
+
+## Research log
+
+Queries run:
+
+- `llms.txt specification llms-full.txt 2026 standard official`
+- `Next.js App Router JSON-LD metadata route handlers sitemap docs`
+- `schema.org LegalCase FAQPage Petition type JSON-LD`
+- `OpenAI GPTBot user agent Anthropic ClaudeBot PerplexityBot user agent docs`
+- `Google AI crawler user agents Gemini robots.txt Google-Extended GoogleOther official documentation`
+
+Current docs checked:
+
+- `https://llmstxt.org/` - root `/llms.txt` proposal, Markdown format, curated link lists, optional `.md` mirrors, and note that `llms.txt` complements sitemap/robots rather than replacing them. Published 2024-09-03.
+- `https://nextjs.org/docs/app/guides/json-ld` - Next recommends native `
-