SLOPSHOPPER

retry-analyzer

Detects Claude repeating the same failing tool call (3+ identical failures, or 4+ with the same error) and tells it to change strategy and tell the user. Never…

newguardprompt
v0.1.1no licenseupdated 2026-10-08oleksandrkodua/qa-mods/retry-analyzer
A shopper browsing a rack in a slop shop
README

qa-mods

Fourteen Claude Code mods, packaged as one plugin marketplace (qa-mods). Made for a manual QA workflow: guard rails around risky commands, evidence for what was tested, and a status band above the prompt.

Install

Run in your own Terminal (a sandboxed Claude session cannot write ~/.claude).

claude plugin marketplace add oleksandrkodua/qa-mods
claude plugin install hud@qa-mods

Replace hud with any plugin from the table.

To install everything from a local clone, or to set up a fresh machine, see INSTALL.md and install-pack.sh.

Plugins

PluginWhat it does
sandbox-guardDenies writes to protected Claude config paths and hands back a Terminal command
remote-gateAsks before git push and wrangler --remote, showing folder, branch, remote and the commits that will go out
blast-radiusShows what a risky Bash command will touch before it runs
retry-analyzerDetects repeated identical failing tool calls and tells Claude to change strategy
secret-redactorMasks tokens and keys in tool output
evidence-saverSaves screenshots from tool results to a folder you pick
replay-theater/replay: step through the session's file edits one by one
verification-guardChecks claims like "tests pass" against tool calls that actually ran
handoffOffers /handoff as the context fills up; fills the prompt box with a HANDOFF.md prompt
notifyA toast and a macOS notification when a long command or turn finishes
quick-actionsCompact and Clear buttons, each asks to confirm first
hudContext fill and rate-limit windows above the prompt, a кеш 59:48 chip (m:ss) while the prompt cache is warm, five buttons, and /hud with the figures as text
next-steps-ukFork of next-steps (MIT, Thariq Shihipar): suggested next prompts are always in Ukrainian. Install instead of next-steps
plan-progressFork of plan-progress 0.7.6 (zycck, MIT): live progress bars above the prompt. Install instead of plan-progress@zycck-mods

Tests

claude plugin test hud                                              # from the repo root
cd hud && node --test tests/format.test.mjs tests/handoff-prompt.test.mjs

A bare node --test also picks up register.test.ts, which only runs under claude plugin test.

License

No license file for the original mods yet. The two forks keep their upstream MIT notices: next-steps-uk/NOTICE.md and plan-progress/NOTICE.md (plus plan-progress/LICENSE.upstream).

Source 2 files
hooks/register.ts 32 lines
1import type { Register } from 'claude-code'
2
3import { errorKey, nudge, signature, Streaks } from './loop'
4
5/**
6 * Retry Analyzer: spots Claude hammering a failing call and tells it to change strategy.
7 * The note rides in `context` (or the deny text) so the model relays it; nothing is blocked.
8 */
9export const register: Register = on => {
10  const streaks = new Streaks()
11
12  on('prompt.submit', ($, e, next) => {
13    streaks.reset() // the user intervened: start counting afresh
14
15    return next(e)
16  })
17
18  on('tool.call', async ($, e, next) => {
19    const ran = await next(e)
20    const denied = ran.deny !== undefined
21    const failed = denied || ran.isError === true
22    const err = denied ? `deny|${String(ran.deny).slice(0, 80)}` : errorKey(String(e.tool), ran.isError === true ? ran.text : '')
23    const hit = streaks.record(`${e.tool}|${signature(e as Record<string, unknown>)}`, err, failed)
24
25    if (!hit) return ran
26
27    return denied
28      ? { deny: `${ran.deny}\n\n${nudge(hit)}` }
29      : { ...ran, context: [...(ran.context ?? []), nudge(hit)] }
30  })
31}
32
hooks/loop.ts 47 lines
1export const SAME_CALL = 3
2export const SAME_ERROR = 4
3
4/** Stable key of a call: tool plus its arguments, minus the per-call ids and the free-text `description` (it changes between identical retries). */
5export function signature(e: Record<string, unknown>): string {
6  const rest = Object.fromEntries(Object.entries(e).filter(([k]) => k !== 'tool_use_id' && k !== 'agentId' && k !== 'description'))
7
8  return JSON.stringify(rest).replace(/\s+/g, ' ').slice(0, 400)
9}
10
11export const errorKey = (tool: string, text: unknown) =>
12  `${tool}|${String(text ?? '').replace(/\d+/g, '#').replace(/\s+/g, ' ').slice(0, 80)}`
13
14export class Streaks {
15  private calls = new Map<string, number>()
16  private errors = new Map<string, number>()
17
18  /** Record one outcome; returns the count that tripped a threshold, or 0. */
19  record(sig: string, err: string, failed: boolean): { count: number; why: 'call' | 'error' } | null {
20    if (!failed) {
21      this.calls.delete(sig)
22      return null
23    }
24
25    const c = (this.calls.get(sig) ?? 0) + 1
26    const x = (this.errors.get(err) ?? 0) + 1
27
28    this.calls.set(sig, c)
29    this.errors.set(err, x)
30
31    if (c >= SAME_CALL) return { count: c, why: 'call' }
32    if (x >= SAME_ERROR) return { count: x, why: 'error' }
33
34    return null
35  }
36
37  reset() {
38    this.calls.clear()
39    this.errors.clear()
40  }
41}
42
43export const nudge = (hit: { count: number; why: 'call' | 'error' }) =>
44  `retry-analyzer: ${
45    hit.why === 'call' ? `this exact call has now failed ${hit.count} times in a row` : `the same error has now appeared ${hit.count} times`
46  }. Stop repeating it: re-read the error, change the approach (different command or inputs, or investigate the cause first), and tell the user in one line what you are changing.`
47