Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
71ee5d0
/plaintiffs: compress above-form copy to Mike's outline (Variant 1)
mikepsinn May 16, 2026
e7d5dce
hooks: enforce codex-background, copy-review, theory-of-mind on copy …
mikepsinn May 16, 2026
74f1115
/treaty: mobile slider fixes + 'health and wealth' gloss (surface-local)
mikepsinn May 16, 2026
a016db3
llms.txt + AI-search defensibility infra
mikepsinn May 16, 2026
0746dd0
ci: job-level paths-filter on web-e2e-validate
mikepsinn May 16, 2026
821af55
hooks: address CodeRabbit/Copilot/claude-review feedback on PR #84
mikepsinn May 16, 2026
b0bd2e4
/plaintiffs: match manual URL casing to project convention
mikepsinn May 16, 2026
c37160d
/dashboard: remove duplicate in-page logout button + 4B plan files
mikepsinn May 16, 2026
4b8c08f
/humanity-v-government: 4 surface fixes per Mike's preview review
mikepsinn May 17, 2026
c3e246e
UX cleanup batch: dead CTAs, 404 recovery, /endorse reorder, /survey …
mikepsinn May 17, 2026
0361ebd
/dashboard: add per-direct-referral downstream count to chain stats
mikepsinn May 17, 2026
0765f5b
hooks: enforce no Codex attribution in commit messages
mikepsinn May 17, 2026
f7d7f34
hooks: SessionStart kills lingering codex.exe processes
mikepsinn May 17, 2026
0cbb648
Clean campaign automation and agent metadata
mikepsinn May 17, 2026
487ed89
Bundle: profile public tasks + org email rewrite + survey embed infra…
mikepsinn May 17, 2026
6261b50
fix(post-vote-share email): restore 'Forward this message' lead per M…
mikepsinn May 17, 2026
400fa32
fix post-vote share copy typos
mikepsinn May 17, 2026
a2b39fc
fix(post-vote-share email): restore COPY THIS MESSAGE phrase alongsid…
mikepsinn May 17, 2026
a12b85f
test(email-post-vote-share): align assertions with current email body
mikepsinn May 17, 2026
afd8f74
post-vote-share: dedup forward instructions + delete REFERRAL_SHARE_L…
mikepsinn May 17, 2026
afe27ee
Polish campaign task actions and case copy
mikepsinn May 17, 2026
416bfa9
Address campaign PR review findings
mikepsinn May 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions .claude/hooks/enforce-codex-background.mjs
Original file line number Diff line number Diff line change
@@ -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 <uuid>` 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);
}
212 changes: 212 additions & 0 deletions .claude/hooks/enforce-copy-review-before-commit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#!/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\//,
];
Comment thread
mikepsinn marked this conversation as resolved.

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. Use a negative lookahead so
// `commit-tree`, `commit-graph`, etc. are excluded (the `\b` boundary
// alone fires on `commit-tree` because `-` is a non-word char). Also
// allow common config prefixes like `git -c user.email=foo commit`,
// `git -C path commit`, `git -S commit`.
if (!/\bgit\s+(?:(?:-[CcP]\s+\S+|-S(?:\s*\S+)?)\s+)*commit(?!\S)/.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;
}
// AskUserQuestion responses come through as text-content user
// messages prefixed with "User has answered your questions:". They
// are NOT new human-initiated messages — without this skip, every
// copy-review confirmation click resets the lastHumanIndex pointer,
// creating an infinite re-ask loop (Mike caught this 2026-05-17
// after 3 forced re-confirmations of the same diff).
const text =
typeof content === "string"
? content
: Array.isArray(content)
? content
.map((p) =>
typeof p?.text === "string" ? p.text : "",
)
.join("")
: "";
if (/^\s*User has answered your questions:/i.test(text)) 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?
//
// Require explicit labeled markers matching the template the hook
// prints on failure. The prior "any `before` near any `after` within
// 400 chars" was a near-no-op — incidental prose like "before commit,
// review changes; after that, ship" satisfied it, and "the header was
// too long; now shorter" satisfied the `was…now` fallback. Mike's
// template uses **BEFORE:** / **AFTER:** so we require those tokens
// (case-insensitive, optional markdown bolding) — they don't appear
// in narrative prose.
const ctRaw = chatText; // keep case for marker detection
const ct = ctRaw.toLowerCase();
const hasBeforeMarker = /(\*\*|__|^|\n)\s*before\s*[:\*]/i.test(ctRaw);
const hasAfterMarker = /(\*\*|__|^|\n)\s*after\s*[:\*]/i.test(ctRaw);
const hasOldNewMarkers =
/(\*\*|__|^|\n)\s*old\s*[:\*]/i.test(ctRaw) &&
/(\*\*|__|^|\n)\s*new\s*[:\*]/i.test(ctRaw);
const showsBeforeAfter =
(hasBeforeMarker && hasAfterMarker) || hasOldNewMarkers;

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:** <verbatim old copy>\n\n` +
` **AFTER:** <verbatim new copy>\n\n` +
` Predicted complaints:\n` +
` 1. **A: Looks good, ship it**\n` +
` 2. **B: <predicted issue 1>**\n` +
` 3. **C: <predicted issue 2>**\n` +
` 4. **D: <predicted issue 3>**\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);
}
Loading
Loading