@hopla/claude-setup 1.17.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.claude-plugin/marketplace.json +2 -1
  2. package/.claude-plugin/plugin.json +4 -3
  3. package/CHANGELOG.md +123 -0
  4. package/LICENSE +21 -0
  5. package/README.md +65 -39
  6. package/agents/system-reviewer.md +4 -4
  7. package/cli.js +288 -8
  8. package/commands/archive.md +137 -0
  9. package/commands/execute.md +1 -1
  10. package/commands/guides/ai-optimized-codebase.md +5 -1
  11. package/commands/guides/data-audit.md +4 -0
  12. package/commands/guides/hooks-reference.md +4 -0
  13. package/commands/guides/mcp-integration.md +6 -2
  14. package/commands/guides/remote-coding.md +5 -1
  15. package/commands/guides/review-checklist.md +4 -0
  16. package/commands/guides/scaling-beyond-engineering.md +4 -0
  17. package/commands/guides/validation-pyramid.md +5 -1
  18. package/commands/guides/write-skill.md +4 -0
  19. package/commands/init-project.md +38 -17
  20. package/commands/plan-feature.md +21 -3
  21. package/commands/system-review.md +11 -15
  22. package/commands/validate.md +1 -1
  23. package/hooks/prompt-route.js +244 -91
  24. package/hooks/session-prime.js +12 -4
  25. package/hooks/statusline.js +3 -5
  26. package/hooks/tsc-check.js +40 -3
  27. package/package.json +16 -6
  28. package/skills/brainstorm/SKILL.md +18 -2
  29. package/skills/code-review/SKILL.md +5 -0
  30. package/skills/code-review/checklist.md +1 -1
  31. package/skills/debug/SKILL.md +1 -1
  32. package/skills/execution-report/SKILL.md +1 -1
  33. package/skills/execution-report/report-structure.md +2 -2
  34. package/skills/git/commit.md +2 -2
  35. package/skills/git/pr.md +1 -1
  36. package/skills/hook-audit/SKILL.md +135 -0
  37. package/skills/hook-audit/checklist.md +210 -0
  38. package/skills/hook-audit/tests/fixtures/use-bad.ts.example +53 -0
  39. package/skills/hook-audit/tests/fixtures/use-good.ts.example +64 -0
  40. package/skills/hook-audit/tests/manual-test.sh +73 -0
  41. package/skills/performance/SKILL.md +1 -1
  42. package/skills/prime/SKILL.md +2 -2
  43. package/skills/refactoring/SKILL.md +1 -1
  44. package/skills/subagent-execution/SKILL.md +2 -2
  45. package/skills/verify/SKILL.md +1 -1
  46. package/skills/worktree/SKILL.md +2 -2
@@ -7,6 +7,25 @@ import { execSync } from "child_process";
7
7
  import fs from "fs";
8
8
  import path from "path";
9
9
 
10
+ const TS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs"];
11
+
12
+ // Pulls every file_path the hook payload references. Covers Write/Edit (single
13
+ // file_path) and MultiEdit (edits[]). Returns null when no payload was available
14
+ // — callers treat null as "can't tell, run tsc to be safe".
15
+ function extractFilePaths(payload) {
16
+ const input = payload?.tool_input;
17
+ if (!input) return null;
18
+ const collected = new Set();
19
+ if (typeof input.file_path === "string") collected.add(input.file_path);
20
+ if (typeof input.path === "string") collected.add(input.path);
21
+ if (Array.isArray(input.edits)) {
22
+ for (const edit of input.edits) {
23
+ if (edit && typeof edit.file_path === "string") collected.add(edit.file_path);
24
+ }
25
+ }
26
+ return collected.size > 0 ? [...collected] : null;
27
+ }
28
+
10
29
  function resolveTscCommand(cwd) {
11
30
  const localBin = path.join(cwd, "node_modules", ".bin", "tsc");
12
31
  if (fs.existsSync(localBin)) return `"${localBin}"`;
@@ -16,10 +35,30 @@ function resolveTscCommand(cwd) {
16
35
  }
17
36
 
18
37
  async function main() {
19
- // Drain stdin (hook contract) but we don't need the payload for tsc --noEmit
20
38
  const chunks = [];
21
39
  for await (const chunk of process.stdin) chunks.push(chunk);
22
40
 
41
+ let payload = null;
42
+ if (chunks.length > 0) {
43
+ try {
44
+ payload = JSON.parse(Buffer.concat(chunks).toString());
45
+ } catch {
46
+ // Malformed payload — fall through; treat as "can't tell"
47
+ }
48
+ }
49
+
50
+ // Filter: if every touched file is non-TS/JS, skip invoking tsc.
51
+ // When the payload is missing or unparseable we default to running tsc
52
+ // (safe behavior — matches the pre-filter implementation).
53
+ const touched = extractFilePaths(payload);
54
+ if (touched !== null) {
55
+ const hasCompilable = touched.some((f) => {
56
+ const ext = path.extname(f).toLowerCase();
57
+ return TS_EXTENSIONS.includes(ext);
58
+ });
59
+ if (!hasCompilable) process.exit(0);
60
+ }
61
+
23
62
  const cwd = process.cwd();
24
63
  const tsconfigPath = path.join(cwd, "tsconfig.json");
25
64
  if (!fs.existsSync(tsconfigPath)) {
@@ -31,14 +70,12 @@ async function main() {
31
70
 
32
71
  try {
33
72
  execSync(`${tsc} --noEmit`, { cwd, stdio: "pipe" });
34
- // Compile clean — exit silently
35
73
  process.exit(0);
36
74
  } catch (err) {
37
75
  const output = (err.stdout || "").toString() + (err.stderr || "").toString();
38
76
  if (output.trim()) {
39
77
  process.stdout.write("TypeScript errors detected:\n" + output + "\n");
40
78
  }
41
- // PostToolUse hooks cannot block; stdout is fed back to Claude
42
79
  process.exit(0);
43
80
  }
44
81
  }
package/package.json CHANGED
@@ -1,12 +1,19 @@
1
1
  {
2
2
  "name": "@hopla/claude-setup",
3
- "version": "1.17.1",
4
- "description": "Hopla team agentic coding system for Claude Code",
3
+ "version": "2.1.1",
4
+ "description": "Agentic coding system for Claude Code — CLI for global rules + permissions (plugin available via /plugin marketplace add HOPLAtools/claude-setup)",
5
5
  "type": "module",
6
6
  "bin": {
7
- "claude-setup": "cli.js",
8
7
  "hopla-claude-setup": "cli.js"
9
8
  },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/HOPLAtools/claude-setup.git"
12
+ },
13
+ "homepage": "https://github.com/HOPLAtools/claude-setup#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/HOPLAtools/claude-setup/issues"
16
+ },
10
17
  "files": [
11
18
  "cli.js",
12
19
  "global-rules.md",
@@ -14,11 +21,14 @@
14
21
  "skills/",
15
22
  "agents/",
16
23
  "hooks/",
17
- ".claude-plugin/"
24
+ ".claude-plugin/",
25
+ "LICENSE",
26
+ "CHANGELOG.md"
18
27
  ],
19
28
  "scripts": {
20
- "prepublishOnly": "node scripts/check-versions.js",
21
- "check-versions": "node scripts/check-versions.js"
29
+ "prepublishOnly": "node scripts/check-versions.js && npm test",
30
+ "check-versions": "node scripts/check-versions.js",
31
+ "test": "node --test tests/*.test.js tests/hooks/*.test.js"
22
32
  },
23
33
  "engines": {
24
34
  "node": ">=18"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: brainstorm
3
- description: "Design exploration and brainstorming before planning. Use when the user wants to explore options for a new feature, discuss approaches, design a solution, brainstorm ideas, or evaluate trade-offs. Trigger on: 'new feature', 'brainstorm', 'explore options', 'design', 'how should we', 'what approach', 'trade-offs', 'quiero agregar', 'diseñar', 'explorar opciones'. Do NOT use when the user already has a clear plan or is asking to execute existing work."
3
+ description: "Design exploration and brainstorming before planning. Use when the user wants to explore options for a new feature, discuss approaches, design a solution, brainstorm ideas, or evaluate trade-offs. Trigger on: 'new feature', 'brainstorm', 'explore options', 'design', 'how should we', 'what approach', 'trade-offs'. Do NOT use when the user already has a clear plan or is asking to execute existing work."
4
4
  ---
5
5
 
6
6
  # Brainstorming: Design Exploration Before Planning
@@ -14,7 +14,7 @@ Explore the problem space and arrive at a validated design BEFORE creating an im
14
14
  ## Process
15
15
 
16
16
  ### Step 1: Explore Context
17
- - Read the project's CLAUDE.md, README, and relevant source files
17
+ - Read the project's AGENTS.md (or CLAUDE.md as fallback), README, and relevant source files
18
18
  - Understand the existing architecture, patterns, and conventions
19
19
  - Identify what already exists that relates to this feature
20
20
  - Check `.agents/plans/` for any related previous work
@@ -61,6 +61,20 @@ Which approach and why
61
61
  ## Design
62
62
  Conceptual design details
63
63
 
64
+ ## Requirements Delta
65
+ > **Include only when the feature changes documented system behavior** — i.e. it adds, modifies, or removes a user-visible capability or business rule. Pure refactors, perf fixes, and infra changes can omit this section. The delta is consumed by `/hopla:archive` to fold the change into `.agents/specs/canonical/`.
66
+
67
+ ### ADDED Requirements
68
+ - REQ-<DOMAIN>-<NNN>: <short title>
69
+ - Scenario: <name> — Given <state>, When <action>, Then <outcome>
70
+
71
+ ### MODIFIED Requirements
72
+ - REQ-<DOMAIN>-<NNN>: <short title> (replaces previous version)
73
+ - <description of how the requirement changes>
74
+
75
+ ### REMOVED Requirements
76
+ - REQ-<DOMAIN>-<NNN>: <short title> (deprecated — reason)
77
+
64
78
  ## Files Affected
65
79
  - List of files to create/modify
66
80
 
@@ -75,6 +89,8 @@ Conceptual design details
75
89
  Run `/hopla:plan-feature` to create the implementation plan from this design
76
90
  ```
77
91
 
92
+ > **Requirement IDs:** use a stable convention like `REQ-<DOMAIN>-<NNN>` (e.g. `REQ-AUTH-002`). The domain prefix should match the canonical spec filename (`auth.md` → `REQ-AUTH-*`). When in doubt about IDs in a brand-new project, leave them as `REQ-AUTH-TBD` and pick numbers when the first canonical spec is created.
93
+
78
94
  ### Step 6: Review Loop
79
95
  Present the design document for user review.
80
96
  - Accept feedback using the `<? ... >` comment syntax
@@ -1,6 +1,11 @@
1
1
  ---
2
2
  name: code-review
3
3
  description: "Technical code review on changed files. Use when the user says 'review code', 'code review', 'check my code', 'review changes', 'look for bugs', or 'audit code'. Also use after completing implementation when validation passes. Do NOT use for reviewing plans or documents — only code."
4
+ triggers:
5
+ - "review (my |the |this )?code"
6
+ - "code review"
7
+ - "audit (my |the |this )?code"
8
+ - "look for bugs"
4
9
  allowed-tools: Read, Grep, Glob, Bash
5
10
  ---
6
11
 
@@ -35,7 +35,7 @@ Apply every category to every changed file. Severity guidance is in the parent `
35
35
 
36
36
  ## 5. Pattern Adherence
37
37
 
38
- - Follows project conventions from `CLAUDE.md`
38
+ - Follows project conventions from `AGENTS.md` (or `CLAUDE.md` as fallback)
39
39
  - Consistent with existing codebase style
40
40
 
41
41
  ## 6. Route & Middleware Ordering
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: debug
3
- description: "Systematic debugging methodology for finding and fixing bugs. Use when encountering errors, bugs, failures, unexpected behavior, or when the user says 'bug', 'error', 'not working', 'failing', 'debug', 'fix', 'broken', 'falla', 'no funciona', 'error'. Do NOT use for planned feature work or refactoring — only for diagnosing and fixing unexpected problems."
3
+ description: "Systematic debugging methodology for finding and fixing bugs. Use when encountering errors, bugs, failures, unexpected behavior, or when the user says 'bug', 'error', 'not working', 'failing', 'debug', 'fix', 'broken'. Do NOT use for planned feature work or refactoring — only for diagnosing and fixing unexpected problems."
4
4
  ---
5
5
 
6
6
  # Systematic Debugging
@@ -39,7 +39,7 @@ Use the full structure documented in `report-structure.md` (same directory). It
39
39
  - Divergences from plan
40
40
  - Scope assessment
41
41
  - Skipped items
42
- - Technical patterns discovered (with ready-to-paste CLAUDE.md entry)
42
+ - Technical patterns discovered (with ready-to-paste AGENTS.md / CLAUDE.md entry)
43
43
  - Recommendations
44
44
 
45
45
  Read `report-structure.md` before writing so every section is filled correctly.
@@ -75,7 +75,7 @@ New gotchas, patterns, or conventions learned during this implementation that sh
75
75
 
76
76
  - **Pattern/Gotcha:** [description]
77
77
  - **Where it applies:** [what type of feature or context triggers this]
78
- - **Ready-to-paste CLAUDE.md entry:** [Write the EXACT text that should be added to the project's CLAUDE.md to prevent this gotcha in future features. Format it as a bullet point under the appropriate section. If it belongs in a guide instead, write the exact text for the guide. Do not write vague suggestions like "document this" — write the actual content so the system reviewer can apply it directly.]
78
+ - **Ready-to-paste AGENTS.md / CLAUDE.md entry:** [Write the EXACT text that should be added to the project's AGENTS.md (or CLAUDE.md for legacy projects) to prevent this gotcha in future features. Format it as a bullet point under the appropriate section. If it belongs in a guide instead, write the exact text for the guide. Do not write vague suggestions like "document this" — write the actual content so the system reviewer can apply it directly.]
79
79
 
80
80
  If nothing new was discovered, write "No new patterns discovered."
81
81
 
@@ -85,4 +85,4 @@ Based on this implementation, what should change for next time?
85
85
 
86
86
  - Plan command improvements: [suggestions]
87
87
  - Execute command improvements: [suggestions]
88
- - CLAUDE.md additions: [suggestions]
88
+ - AGENTS.md / CLAUDE.md additions: [suggestions]
@@ -47,14 +47,14 @@ Wait for explicit approval before running `git commit`.
47
47
 
48
48
  ## Step 5: Version Bump (if configured)
49
49
 
50
- Before committing, check the project's `CLAUDE.md` for a `## Versioning` section. If it exists:
50
+ Before committing, check the project's `AGENTS.md` (or `CLAUDE.md` as fallback) for a `## Versioning` section. If it exists:
51
51
 
52
52
  1. Read the versioning configuration (command, trigger, files)
53
53
  2. Check if the **trigger condition** matches (e.g., specific branches, always, etc.)
54
54
  3. If it matches, run the version bump command
55
55
  4. Stage the version files alongside the other changes
56
56
 
57
- If no `## Versioning` section exists in the project's `CLAUDE.md`, skip this step entirely.
57
+ If no `## Versioning` section exists in the project's `AGENTS.md` / `CLAUDE.md`, skip this step entirely.
58
58
 
59
59
  ## Step 6: Execute Commit
60
60
 
package/skills/git/pr.md CHANGED
@@ -15,7 +15,7 @@ git diff --stat origin/$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>
15
15
 
16
16
  Read the following if they exist:
17
17
  - The plan file in `.agents/plans/` related to this feature
18
- - `CLAUDE.md` — for project context
18
+ - `AGENTS.md` (or `CLAUDE.md` as fallback) — for project context
19
19
 
20
20
  ## Step 2: Determine Base Branch
21
21
 
@@ -0,0 +1,135 @@
1
+ ---
2
+ name: hook-audit
3
+ description: "Static audit of new React hooks against documented bug-class catalog (memoization, stale-id guards, error-match strictness, cache+dedup integrity). Use after creating any file matching `src/hooks/use*.ts` and BEFORE commit. Trigger words: 'audit hook', 'check hook', 'hook review'. Auto-callable from execute skill's Level 1.5 gate."
4
+ allowed-tools: Read, Grep, Glob, Bash
5
+ ---
6
+
7
+ > 🌐 **Language:** All user-facing output must match the user's language. Code, paths, and commands stay in English.
8
+
9
+ Mechanical static audit of React hook files (`src/hooks/use*.ts`) against the four recurring bug classes catalogued in `checklist.md` (same directory). This skill is FAST (<5s per file) and structural — it does not interpret intent. For semantic / interpretive review, use `code-review` instead.
10
+
11
+ **When to invoke:**
12
+ - Right after creating or significantly modifying a `src/hooks/use*.ts` file
13
+ - BEFORE committing the hook
14
+ - Auto-callable from `commands/execute.md` Level 1.5 gate (companion plan: `plan-feature-04c-improvements.md`)
15
+
16
+ ## Step 1: Identify Target File(s)
17
+
18
+ Two modes:
19
+
20
+ 1. **Single-file mode** — argument provided:
21
+ ```bash
22
+ /hopla:hook-audit src/hooks/useFoo.ts
23
+ ```
24
+ Treat the argument as a single path. If the file does not exist, exit with `File not found: <path>`.
25
+
26
+ 2. **Auto-detect mode** — no argument:
27
+ ```bash
28
+ git diff --name-only HEAD | grep -E '^src/hooks/use[A-Z][A-Za-z0-9_]*\.ts$'
29
+ ```
30
+ Collect the matching files. If the result is empty:
31
+ ```bash
32
+ git ls-files --others --exclude-standard | grep -E '^src/hooks/use[A-Z][A-Za-z0-9_]*\.ts$'
33
+ ```
34
+ to pick up new untracked hook files. If both are empty, exit cleanly with `No hook files to audit.`
35
+
36
+ Scope is strict: only `src/hooks/use*.ts`. Files matching `src/lib/use*` or `electron/use*` are NOT audited (see Out of Scope in the plan).
37
+
38
+ ## Step 2: Load the Checklist
39
+
40
+ Read `checklist.md` in this skill's directory. It documents each rule with:
41
+ - Rule code (P-5 / S-8 / E-1 / D-1)
42
+ - Severity
43
+ - Detection pattern (regex or grep command)
44
+ - Fix suggestion (with code example)
45
+ - Canonical reference (file from consumer-project history)
46
+
47
+ ## Step 3: Apply Each Rule
48
+
49
+ For each target file, run the detection command for every rule (see `checklist.md` for the exact commands). Collect findings.
50
+
51
+ Detection commands must be portable across BSD grep (macOS) and GNU grep (Linux). Use `grep -E` for ERE syntax. Avoid GNU-only flags (`-P`, `-z`).
52
+
53
+ Each finding records:
54
+ - File path
55
+ - Line number (use `grep -nE` to get line numbers)
56
+ - Rule code
57
+ - One-line description
58
+ - Fix suggestion (from `checklist.md`)
59
+ - Canonical reference
60
+
61
+ ## Step 4: Output Report
62
+
63
+ Group findings by file. Format per finding:
64
+
65
+ ```
66
+ [severity] [rule-code] file:line — description
67
+ Fix: suggestion
68
+ Reference: canonical-file.ts (commit-hash)
69
+ ```
70
+
71
+ End with a one-line summary:
72
+ ```
73
+ N issues found across M files. K rules clean.
74
+ ```
75
+
76
+ If no issues found, exit with exactly:
77
+ ```
78
+ Hook audit clean — 4 rules checked.
79
+ ```
80
+
81
+ **Gate output budget:** When invoked from `execute.md` Level 1.5 gate, the report must be under 50 lines. If more than 5 issues per file, show a count + the top 3 findings with detail; collapse the rest as `+N more (same rule)`.
82
+
83
+ ## Step 5: Exit Code
84
+
85
+ This skill is markdown — it does not exit directly. When the calling agent invokes the skill from `execute.md`'s gate (via Bash), the calling Bash invocation should:
86
+ - Exit `0` when the summary line shows `Hook audit clean`
87
+ - Exit `1` when any issues are reported
88
+
89
+ The gate (defined in `plan-feature-04c-improvements.md`) decides whether to fail the commit or merely warn based on severity. This skill's job is to REPORT — not to enforce.
90
+
91
+ ## Example Output
92
+
93
+ **Clean run** (0 issues):
94
+ ```
95
+ $ /hopla:hook-audit src/hooks/useGradingDefinitions.ts
96
+ Hook audit clean — 4 rules checked.
97
+ ```
98
+
99
+ **3-issue run:**
100
+ ```
101
+ $ /hopla:hook-audit src/hooks/useFooLookup.ts
102
+
103
+ [high] [P-5] src/hooks/useFooLookup.ts:142 — Hook returns object literal without useMemo
104
+ Fix: Wrap the return in useMemo({ ... }, [field1, field2, ...])
105
+ Reference: useGradingDefinitions.ts:123-127
106
+
107
+ [high] [S-8] src/hooks/useFooLookup.ts:88 — `setLoading(false)` inside stale-id guard
108
+ Fix: Move setLoading(false) out of `if (currentFooRef.current === foo)` block in the finally
109
+ Reference: useSickwLookup.ts (commit 228cd6a)
110
+
111
+ [medium] [E-1] src/hooks/useFooLookup.ts:101 — Error matching uses substring `.includes('imei')`
112
+ Fix: Replace with exact match or anchored regex (`/^imei[: ]/`)
113
+ Reference: useImeiInOtherShipment.ts post-fix
114
+
115
+ 3 issues found across 1 file. 1 rule clean (D-1).
116
+ ```
117
+
118
+ ## Source Incidents
119
+
120
+ This skill exists because the same four bug classes recurred across consumer projects. Each rule in `checklist.md` cites the canonical fixed sibling.
121
+
122
+ | Occurrence | File | Rule(s) | Fix commit |
123
+ |---|---|---|---|
124
+ | 1 | PhoneTest `useSickwLookup` | S-8 | 228cd6a |
125
+ | 2 | PhoneTest `useIfreeicloudLookup` | S-8 | 06cc692 |
126
+ | 3 | ifreeicloud-library `useIfreeicloudLookup` (second pass) | P-5, S-8 | — |
127
+ | 4 | 04c `useImeiInOtherShipment`, `useActiveShipment` | P-5, S-8, E-1 | 952cab1 |
128
+
129
+ Recurrence count = motivation for a dedicated mechanical pre-flight skill, separate from the broader `code-review`.
130
+
131
+ ## Next Step
132
+
133
+ After the audit, the calling agent (execute, or the user via slash command) should:
134
+ - If clean: proceed to commit.
135
+ - If issues found: fix per the suggestions, then re-run this skill until clean. For larger refactors, escalate to `code-review` for full semantic review.
@@ -0,0 +1,210 @@
1
+ # Hook Audit Checklist
2
+
3
+ Four mechanical rules. Each rule has a portable detection command (BSD + GNU grep), a fix suggestion, and a canonical reference from consumer-project history.
4
+
5
+ > **Maintainer note on canonical references:** The cited commit hashes and file names come from a specific consumer project (PhoneTest-Desktop-App). When this skill is used in other consumer projects, the references may not resolve locally — that's fine, they are documentation of the pattern's origin, not a hard dependency. Update them as patterns evolve (revisit when promoting to v2).
6
+
7
+ ---
8
+
9
+ ## Rule P-5 — Hook return must be memoized
10
+
11
+ **Severity:** high
12
+
13
+ **What it catches:** A hook function that ends with `return { ... };` (object literal) NOT wrapped in `useMemo`. Every render returns a new object, which invalidates downstream `useMemo` / `useEffect` deps that consume the hook's return, causing re-render storms and stale-closure bugs.
14
+
15
+ **Scope filter (apply BEFORE flagging):** The rule applies only to the FINAL return statement of a function whose name starts with `use` and that calls React hooks (`useState`, `useRef`, `useMemo`, `useEffect`, etc.). Plain helper functions inside the same file (e.g. `async function fakeFetch(...)`) are NOT subject to this rule even if they return an object literal.
16
+
17
+ **Detection (portable grep):**
18
+
19
+ ```bash
20
+ # Step 1 — collect candidate `return { ... }` lines (heuristic; may include helpers)
21
+ grep -nE 'return[[:space:]]*\{' "$FILE"
22
+ ```
23
+
24
+ For each match, read the surrounding 20 lines and apply two filters:
25
+ 1. Is the enclosing function a React hook (named `useX`, uses React hooks)? If no → skip.
26
+ 2. Is the `return { ... }` block already inside a `useMemo(() => ({ ... }), [...])` wrapper? If yes → skip.
27
+
28
+ Only flag matches that fail BOTH filters. The wrapper pattern looks like:
29
+
30
+ ```ts
31
+ return useMemo(() => ({
32
+ field1,
33
+ field2,
34
+ }), [field1, field2]);
35
+ ```
36
+
37
+ If the `return { ... }` is NOT inside a `useMemo`, flag it.
38
+
39
+ **Fix:**
40
+
41
+ ```ts
42
+ // Before
43
+ return {
44
+ foo,
45
+ bar,
46
+ doSomething,
47
+ };
48
+
49
+ // After
50
+ return useMemo(() => ({
51
+ foo,
52
+ bar,
53
+ doSomething,
54
+ }), [foo, bar, doSomething]);
55
+ ```
56
+
57
+ Include every field used in the object literal in the dep array. Missing deps cause stale values.
58
+
59
+ **Canonical reference:** `useGradingDefinitions.ts:123-127` (consumer projects). Look for the well-formed `return useMemo(() => ({ ... }), [...])` shape there as the model.
60
+
61
+ ---
62
+
63
+ ## Rule S-8 — `setLoading(false)` must not be gated by stale-id guard
64
+
65
+ **Severity:** high
66
+
67
+ **What it catches:** Inside a `finally` block, `setLoading(false)` is wrapped in an `if (currentXRef.current === X)` guard. The guard correctly prevents stale results from overwriting newer ones, but ALSO blocks `setLoading(false)` from running when the request is superseded — leaving the UI stuck in "loading" forever when the user rapidly switches inputs.
68
+
69
+ **Detection (portable grep):**
70
+
71
+ ```bash
72
+ # Find any line with `setLoading(false)` inside the file, then read 5 lines above to check for an `if (current*Ref.current === ...)` guard
73
+ grep -nE 'setLoading\(false\)' "$FILE"
74
+ ```
75
+
76
+ For each match, read 5–10 lines above and confirm whether the call is enclosed by:
77
+
78
+ ```ts
79
+ if (currentSomethingRef.current === somethingId) {
80
+ // ... result handling ...
81
+ setLoading(false); // BUG: blocked when request is superseded
82
+ }
83
+ ```
84
+
85
+ If the `setLoading(false)` is INSIDE the guard, flag it. If it's OUTSIDE the guard (i.e. after the closing brace), the file is clean for this rule.
86
+
87
+ **Fix:**
88
+
89
+ ```ts
90
+ // Before — bug: setLoading blocked when superseded
91
+ } finally {
92
+ if (currentImeiRef.current === imei) {
93
+ setData(result);
94
+ setLoading(false);
95
+ }
96
+ }
97
+
98
+ // After — setLoading runs unconditionally; only data update is guarded
99
+ } finally {
100
+ if (currentImeiRef.current === imei) {
101
+ setData(result);
102
+ }
103
+ setLoading(false);
104
+ }
105
+ ```
106
+
107
+ **Canonical reference:** `useSickwLookup.ts` after commit `228cd6a` (PhoneTest history). The fix moved `setLoading(false)` out of the guard.
108
+
109
+ ---
110
+
111
+ ## Rule E-1 — Error matching must be anchored, not substring
112
+
113
+ **Severity:** medium
114
+
115
+ **What it catches:** Error message detection that uses `.includes('imei')` or similar substring patterns. A substring match can fire on unrelated error messages that happen to contain the token (e.g. `"server error: imei lookup deferred"` would match an `imei`-specific error handler intended only for `"invalid imei format"`).
116
+
117
+ **Detection (portable grep):**
118
+
119
+ ```bash
120
+ grep -nE "\.message\??\.includes\([\"'][a-z]+[\"']\)" "$FILE"
121
+ ```
122
+
123
+ This regex matches the antipattern `error.message.includes('foo')` (optional chaining tolerated). Each hit is a finding.
124
+
125
+ **Fix:**
126
+
127
+ ```ts
128
+ // Before — substring match (too permissive)
129
+ if (error.message.includes('imei')) {
130
+ // ...
131
+ }
132
+
133
+ // After — anchored regex (precise)
134
+ if (/^(invalid )?imei( format)?\b/i.test(error.message)) {
135
+ // ...
136
+ }
137
+
138
+ // Alternative — exact match if the message is a known constant
139
+ if (error.message === 'Invalid IMEI') {
140
+ // ...
141
+ }
142
+ ```
143
+
144
+ **Canonical reference:** Consumer-project review checklist Section 1, pattern documented after the `useImeiInOtherShipment.ts` error-overshadow incident.
145
+
146
+ ---
147
+
148
+ ## Rule D-1 — Module-level cache must have in-flight dedup
149
+
150
+ **Severity:** medium
151
+
152
+ **What it catches:** A hook file declares a module-level `cache` Map for response caching but does NOT also declare an `inFlight` Map. Without dedup, two parallel calls for the same key race and produce duplicate network requests.
153
+
154
+ **Detection (portable grep):**
155
+
156
+ ```bash
157
+ # Find module-level `const cache = new Map`
158
+ grep -nE '^const cache[[:space:]]*=[[:space:]]*new Map' "$FILE"
159
+ ```
160
+
161
+ If the file matches, check for the companion `inFlight` map:
162
+
163
+ ```bash
164
+ grep -nE '^const inFlight[[:space:]]*=[[:space:]]*new Map' "$FILE"
165
+ ```
166
+
167
+ If `cache` is present but `inFlight` is absent, flag it. Both declarations must be at module scope (no leading whitespace).
168
+
169
+ **Fix:**
170
+
171
+ ```ts
172
+ // Both maps at module scope
173
+ const cache = new Map<string, ResultShape>();
174
+ const inFlight = new Map<string, Promise<ResultShape>>();
175
+
176
+ async function fetchOnce(key: string): Promise<ResultShape> {
177
+ const cached = cache.get(key);
178
+ if (cached) return cached;
179
+
180
+ const inflight = inFlight.get(key);
181
+ if (inflight) return inflight;
182
+
183
+ const promise = doFetch(key).then((result) => {
184
+ cache.set(key, result);
185
+ inFlight.delete(key);
186
+ return result;
187
+ });
188
+ inFlight.set(key, promise);
189
+ return promise;
190
+ }
191
+ ```
192
+
193
+ The companion `inFlight` map ensures parallel calls for the same key share a single in-flight Promise instead of racing.
194
+
195
+ **Canonical reference:** Consumer-project lookup hooks pattern (e.g. `useSickwLookup.ts` post-`228cd6a`).
196
+
197
+ ---
198
+
199
+ ## Portability Notes
200
+
201
+ All detection commands use BRE/ERE syntax supported by both:
202
+ - BSD grep (macOS default — `/usr/bin/grep`)
203
+ - GNU grep (most Linux distributions)
204
+
205
+ Avoid:
206
+ - `-P` (Perl-compatible — GNU only)
207
+ - `-z` (null-separated — GNU only)
208
+ - Lookahead/lookbehind (`(?=...)`, `(?<=...)`)
209
+
210
+ Use `grep -nE` for line numbers + ERE. Use `grep -E` instead of `egrep` (the latter is deprecated on some systems).
@@ -0,0 +1,53 @@
1
+ // SYNTHETIC FIXTURE — triggers all 4 hook-audit rules.
2
+ // Filename is *.example so TypeScript ignores it. This file is read as text by the hook-audit skill.
3
+
4
+ import { useState, useRef } from 'react';
5
+
6
+ // Rule D-1: module-level cache without companion inFlight map. BUG.
7
+ const cache = new Map<string, FooResult>();
8
+
9
+ type FooResult = { ok: boolean; value: string };
10
+
11
+ export function useFooLookup(fooId: string) {
12
+ const [data, setData] = useState<FooResult | null>(null);
13
+ const [loading, setLoading] = useState(false);
14
+ const currentFooRef = useRef<string>(fooId);
15
+
16
+ async function load(id: string) {
17
+ setLoading(true);
18
+ currentFooRef.current = id;
19
+ try {
20
+ const cached = cache.get(id);
21
+ if (cached) {
22
+ setData(cached);
23
+ return;
24
+ }
25
+ const result = await fakeFetch(id);
26
+ cache.set(id, result);
27
+ if (currentFooRef.current === id) {
28
+ setData(result);
29
+ }
30
+ } catch (error: any) {
31
+ // Rule E-1: substring match — too permissive. BUG.
32
+ if (error.message.includes('imei')) {
33
+ setData({ ok: false, value: 'imei-error' });
34
+ }
35
+ } finally {
36
+ // Rule S-8: setLoading(false) inside stale-id guard. BUG.
37
+ if (currentFooRef.current === id) {
38
+ setLoading(false);
39
+ }
40
+ }
41
+ }
42
+
43
+ // Rule P-5: object literal return without useMemo. BUG.
44
+ return {
45
+ data,
46
+ loading,
47
+ load,
48
+ };
49
+ }
50
+
51
+ async function fakeFetch(id: string): Promise<FooResult> {
52
+ return { ok: true, value: id };
53
+ }