SLOPSHOPPER

review-lint

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

newguardtoast
A shopper browsing a rack in a slop shop
README

claude-composio-mods

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/.

  • ticket-band: Shows the Linear ticket for the current branch above the prompt, with one-press Done once every PR has merged.
  • review-chaser: /reviews lists your open PRs waiting on reviewers, with a Slack nudge button per reviewer.
  • snippet: /snippet drafts your daily Slack update from the last 24h of Linear + GitHub and posts it on one press.
  • composio-daily: /standup pane, looks up failing-test errors in Datadog ("seen before"), and sets your Slack status while the agent is busy.
  • link-prefetch: Paste a Slack thread, Linear issue or GitHub PR link and Claude gets its contents as context before it answers.
  • composio-pulse: Times every composio CLI call into a status-line sparkline (/pulse for details) and blocks re-linking a toolkit that's already connected.
  • review-lint: When an edit adds a cache or a brittle test assertion, hands Claude a cache-review checklist or rules against implementation-coupled tests.

Install

/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>.

Requirements

  • The Composio CLI, installed at ~/.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.

Settings

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.

Source 1 files
hooks/register.tsx 44 lines
1import 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