Refuses the first write to each test file with the kinds of test this repo does not want

The St. Olaf community, now in pocket size… rewritten in React Native.
cd into your clonemise install to install the required toolspnpm install --frozen-lockfile to install the required dependenciesmise run ios to launch the app in a simulator, or mise run device "Phone" to build and launch the app on a connected iPhone named "Phone"Welcome!
Thank you for your interest in contributing to AAO-React-Native. First, whenever interacting with the team or in general, please adhere to our Code of Conduct. Next, see CONTRIBUTING.md.
<h6>Apple, the Apple logo, iPhone, and iPad are trademarks of Apple Inc., registered in the U.S. and other countries.<br>App Store is a service mark of Apple Inc., registered in the U.S. and other countries.</h6>
hooks/register.ts 127 lines1import {atom, read, update} from 'claude-code'
2import type {EngineInterface, Register} from 'claude-code'
3
4import type {ReminderKey} from '../types'
5
6/** What the model reads when a write is refused; AGENTS.md → Testing gives the reasons. */
7export const REMINDER = `test-writing-reminder: check this test against the kinds this repo does not want (AGENTS.md → Testing):
8
91. Jest tests of look or layout: color, size, spacing, truncation, tap targets
102. Tests that check mocked behavior
113. Tests that need heavy mocking to run
124. Tests of platform behavior: what iOS, React Native, Hermes or a library does, rather than what this app decides
135. Tests that metrics or Sentry calls are sent
146. Tests that restate config
157. Data-flow UI tests
16
17If the test is none of these, send the same write again.`
18
19/** Paths that mark a file as a test in this repo. */
20const TEST_FILE_PATTERNS = [
21 /(^|\/)__tests__\//,
22 /\.test\.(ts|tsx|mjs)$/,
23 /(^|\/)uitests\/.+\.swift$/,
24 /Tests?\.swift$/,
25]
26
27/** Whether a path names a test file in this repo. */
28export function isTestFile(path: string): boolean {
29 return TEST_FILE_PATTERNS.some((pattern) => pattern.test(path))
30}
31
32/** A word of a shell command, as far as the mod needs: a path, a flag or a command name. */
33const WORD = /[^\s'"`<>|;&()=,]+/g
34
35/**
36 * Signs that a command edits files in place, where any test file it names may
37 * be the one written. Loose on purpose: a false alarm costs one resend, a miss
38 * costs the reminder.
39 */
40const IN_PLACE_EDITS = [
41 /open\([^)]*['"][wa]\+?['"]/,
42 /\bwrite_text\b/,
43 /\bwriteFile(Sync)?\(/,
44 /\bsed\s+(-\w+\s+)*-i\b/,
45 /\bperl\s+(-\w+\s+)*-\w*i/,
46]
47
48/** Whether a shell command looks like it edits files in place. */
49export function editsInPlace(command: string): boolean {
50 return IN_PLACE_EDITS.some((edit) => edit.test(command))
51}
52
53/** Where a redirect sends output: `>`, `>>` or `1>`, but not `2>&1`, `>&2`, `=>` or `->`. */
54const REDIRECT_TARGET = /(?:^|[^>=-])>>?(?!&)\s*['"]?([^\s'"`<>|;&()]+)/g
55
56/**
57 * The files a shell command sends output to: redirect targets, `tee`'s files,
58 * and the last argument of `cp` or `mv`. Only these count for such commands,
59 * so a test run piped through `tee` or a copy out of a test file is no write.
60 */
61export function destinationsIn(command: string): string[] {
62 const redirected = [...command.matchAll(REDIRECT_TARGET)].flatMap((match) => match[1] ?? [])
63 const written = command.split(/[|;&\n]/).flatMap((segment) => {
64 const words = segment.match(WORD) ?? []
65 const [name, ...args] = words[0] === 'git' ? words.slice(1) : words
66 if (name === 'tee') {
67 return args.filter((arg) => !arg.startsWith('-'))
68 }
69 if (name === 'cp' || name === 'mv') {
70 return args.slice(-1)
71 }
72 return []
73 })
74 return [...redirected, ...written]
75}
76
77/** The test files a shell command names, each once. */
78export function testFilesIn(command: string): string[] {
79 const words = command.match(WORD) ?? []
80 return [...new Set(words.filter(isTestFile))]
81}
82
83/** The test files each agent has already been reminded about this session. */
84const reminded = atom(
85 {plugin: 'test-writing-reminder', key: 'reminded'} as const,
86 [] as readonly ReminderKey[],
87)
88
89/**
90 * Marks the test files as reminded for this agent, and says whether any had
91 * not been yet. A subagent never sees the main loop's refusal, so each agent
92 * is reminded on its own.
93 */
94async function shouldRemind(
95 $: EngineInterface,
96 agentId: string | undefined,
97 paths: readonly string[],
98): Promise<boolean> {
99 const keys = paths.map((path) => `${agentId ?? 'main'}:${path.replace(/^\.\//, '')}`)
100 const seen = await read($, reminded)
101 if (keys.every((key) => seen.includes(key))) {
102 return false
103 }
104 await update($, reminded, (list) => [...new Set([...list, ...keys])])
105 return true
106}
107
108export const register: Register = (on) => {
109 on('tool.call', {tool: 'Write'}, async ($, e, next) =>
110 (await shouldRemind($, e.agentId, [e.file_path].filter(isTestFile)))
111 ? {deny: REMINDER}
112 : next(e),
113 )
114
115 on('tool.call', {tool: 'Edit'}, async ($, e, next) =>
116 (await shouldRemind($, e.agentId, [e.file_path].filter(isTestFile)))
117 ? {deny: REMINDER}
118 : next(e),
119 )
120
121 on('tool.call', {tool: 'Bash'}, async ($, e, next) => {
122 const edited = editsInPlace(e.command) ? testFilesIn(e.command) : []
123 const paths = [...edited, ...destinationsIn(e.command).filter(isTestFile)]
124 return (await shouldRemind($, e.agentId, paths)) ? {deny: REMINDER} : next(e)
125 })
126}
127types/index.d.ts 9 lines1/** `<agentId or "main">:<path as written>`: one test file one agent has been reminded about. */
2export type ReminderKey = string
3
4declare module 'claude-code' {
5 interface PluginState {
6 'test-writing-reminder': {reminded: readonly ReminderKey[]}
7 }
8}
9