-
-
Notifications
You must be signed in to change notification settings - Fork 2
Plaintiffs variant 1 and copy-edit protocol hooks #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 e7d5dce
hooks: enforce codex-background, copy-review, theory-of-mind on copy …
mikepsinn 74f1115
/treaty: mobile slider fixes + 'health and wealth' gloss (surface-local)
mikepsinn a016db3
llms.txt + AI-search defensibility infra
mikepsinn 0746dd0
ci: job-level paths-filter on web-e2e-validate
mikepsinn 821af55
hooks: address CodeRabbit/Copilot/claude-review feedback on PR #84
mikepsinn b0bd2e4
/plaintiffs: match manual URL casing to project convention
mikepsinn c37160d
/dashboard: remove duplicate in-page logout button + 4B plan files
mikepsinn 4b8c08f
/humanity-v-government: 4 surface fixes per Mike's preview review
mikepsinn c3e246e
UX cleanup batch: dead CTAs, 404 recovery, /endorse reorder, /survey …
mikepsinn 0361ebd
/dashboard: add per-direct-referral downstream count to chain stats
mikepsinn 0765f5b
hooks: enforce no Codex attribution in commit messages
mikepsinn f7d7f34
hooks: SessionStart kills lingering codex.exe processes
mikepsinn 0cbb648
Clean campaign automation and agent metadata
mikepsinn 487ed89
Bundle: profile public tasks + org email rewrite + survey embed infra…
mikepsinn 6261b50
fix(post-vote-share email): restore 'Forward this message' lead per M…
mikepsinn 400fa32
fix post-vote share copy typos
mikepsinn a2b39fc
fix(post-vote-share email): restore COPY THIS MESSAGE phrase alongsid…
mikepsinn a12b85f
test(email-post-vote-share): align assertions with current email body
mikepsinn afd8f74
post-vote-share: dedup forward instructions + delete REFERRAL_SHARE_L…
mikepsinn afe27ee
Polish campaign task actions and case copy
mikepsinn 416bfa9
Address campaign PR review findings
mikepsinn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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\//, | ||
| ]; | ||
|
|
||
| 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); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.