Hands Claude a cache-review checklist or test rules when an edit adds a cache or a brittle test assertion

Seven Claude Code mods (function-hook plugins) that wire your terminal into Slack, Linear, GitHub and Datadog through the Composio CLI.
Demo: video/out.mp4. The video is built with fframes; its source is in video/.
/reviews lists your open PRs waiting on reviewers, with a Slack nudge button per reviewer./snippet drafts your daily Slack update from the last 24h of Linear + GitHub and posts it on one press./standup pane, looks up failing-test errors in Datadog ("seen before"), and sets your Slack status while the agent is busy.composio CLI call into a status-line sparkline (/pulse for details) and blocks re-linking a toolkit that's already connected./plugin marketplace add tridha643/claude-composio-mods
/plugin install ticket-band@claude-composio-mods
Install whichever mods you want the same way. To hack on them instead, clone the repo and run claude --plugin-dir plugins/<mod>.
~/.composio/composio and logged in, with the toolkits each mod uses connected (Slack, Linear, GitHub, Datadog).gh, logged in, for review-chaser and snippet.review-chaser and snippet search PRs in the ComposioHQ org; change the query in hooks/register.tsx for your own org.Set these from the /plugin config menu:
composio-daily → standup_channel: the Slack channel the standup Post button sends to.snippet → channel: the Slack channel /snippet posts to.ticket-band → team_keys: comma-separated Linear team keys to look for in branch names. Empty matches any KEY-123.composio-pulse → search_budget_ms: marks the median search time ✓ or ✗ against this budget. 0 hides the mark.An empty channel hides that Post button.
hooks/register.tsx 44 lines1import type { EngineInterface, Register, ToolCallResult } from 'claude-code'
2
3// Assertions that couple a test to implementation: call counts, timing, cache-hit and ORM query-shape checks.
4const BRITTLE_TEST = /toHaveBeenCalledTimes|performance\.now\(\)|Date\.now\(\)\s*-|toBeLessThan(OrEqual)?\(\s*\d+\s*\)|\b(cacheHits?|hitCount|misses)\b|toHaveBeenCalledWith\([^)]*\b(findMany|findUnique|findFirst|\$queryRaw)\b/
5// Case-sensitive on purpose: catches userCache / cacheTtlMs / setex without matching "settle".
6const CACHE = /[Cc]ache|setex|[Ee]xpire|\b(lru|LRU|ttl|TTL)|[a-z](Ttl|TTL)|[Mm]emoiz/
7const IS_TEST = /\.(test|spec)\.[cm]?[jt]sx?$/
8const IS_CODE = /\.([cm]?[jt]sx?|go|py|rs)$/
9
10const TEST_RULES = `review-lint: this edit adds an assertion that couples the test to implementation details. Don't assert call counts (toHaveBeenCalledTimes), cache hits, ORM query shapes or wall-clock timings (performance.now() < N). Assert the observable result instead: the returned value, the persisted row, or the response the caller sees. Rework the assertion unless the user asked for exactly this.`
11
12const CACHE_CHECKLIST = `review-lint: this edit introduces caching. Before finishing, make sure the change (code, PR body, or design doc) answers this cache-review checklist:
131. How is hit rate measured? Ship a hit/miss metric so a cache that never hits gets noticed.
142. How is it invalidated, and what is the TTL?
153. How do future code paths that change the underlying data stay invalidating it?
164. If no invalidation is needed, say why.
175. Is it behind a flag with an off or shadow mode for rollback?
18Mention any gaps to the user; don't invent answers.`
19
20// Reminded once per file and rule; resets on reload, which is fine.
21const told = new Set<string>()
22
23function lint(path: string, added: string, removed: string) {
24 if (IS_TEST.test(path)) return BRITTLE_TEST.test(added) && !BRITTLE_TEST.test(removed) ? TEST_RULES : null
25 return IS_CODE.test(path) && CACHE.test(added) && !CACHE.test(removed) ? CACHE_CHECKLIST : null
26}
27
28function remind($: EngineInterface, ran: ToolCallResult, path: string, rule: string | null) {
29 if (!rule || ran.deny !== undefined || ran.isError || told.has(`${path}:${rule}`)) return ran
30 told.add(`${path}:${rule}`)
31 $.ui.toast(rule === TEST_RULES ? `brittle test assertion in ${path.split('/').pop()}` : `new cache in ${path.split('/').pop()}: checklist sent to Claude`)
32 return { ...ran, context: [...(ran.context ?? []), `${rule}\n(file: ${path})`] }
33}
34
35export const register: Register = on => {
36 on('tool.call', { tool: 'Edit' }, async ($, e, next) =>
37 remind($, await next(e), e.file_path, lint(e.file_path, e.new_string, e.old_string)),
38 )
39
40 on('tool.call', { tool: 'Write' }, async ($, e, next) =>
41 remind($, await next(e), e.file_path, lint(e.file_path, e.content, '')),
42 )
43}
44