SLOPSHOPPER

watch-loop

Denies the third consecutive poll or wait command when no edit has landed between them.

newguardtoastprompt
v0.1.0no licenseupdated 2026-10-01howells/howells-plugins/mods/watch-loop
A shopper browsing a rack in a slop shop
Source 1 files
hooks/register.ts 63 lines
1import type { Register } from 'claude-code'
2
3const LIMIT = 3
4const MIN_SLEEP_SECONDS = 10
5
6const WATCH =
7  /\bgh\s+run\s+(watch|view)\b|\bgh\s+pr\s+checks\b|\bvercel\s+(inspect|logs)\b|\buntil\b[^;]*;\s*do\b|\bwhile\b[^;]*;\s*do\b[\s\S]*\bsleep\b/
8
9export const isPoll = (command: string): boolean => {
10  if (WATCH.test(command)) {
11    return true
12  }
13
14  for (const match of command.matchAll(/\bsleep\s+(\d+(?:\.\d+)?)([smh]?)/g)) {
15    const scale = match[2] === 'h' ? 3600 : match[2] === 'm' ? 60 : 1
16
17    if (Number(match[1]) * scale >= MIN_SLEEP_SECONDS) {
18      return true
19    }
20  }
21
22  return false
23}
24
25export const register: Register = on => {
26  const polls = new Map<string, number>()
27
28  on('prompt.submit', ($, e, next) => {
29    polls.clear()
30
31    return next(e)
32  })
33
34  on('tool.call', ($, e, next) => {
35    const loop = e.agentId ?? 'main'
36
37    if (e.tool === 'Edit' || e.tool === 'Write' || e.tool === 'NotebookEdit') {
38      polls.delete(loop)
39
40      return next(e)
41    }
42
43    if (e.tool !== 'Bash' || !isPoll(e.command)) {
44      return next(e)
45    }
46
47    const count = (polls.get(loop) ?? 0) + 1
48
49    if (count < LIMIT) {
50      polls.set(loop, count)
51
52      return next(e)
53    }
54
55    polls.delete(loop)
56    $.ui.toast('watch-loop: held a third poll in a row')
57
58    return {
59      deny: `${$.plugin.name}: third poll or wait in a row with no edit between. Polling ships nothing. Do other work now and check once at the end of the expected duration, or say what is blocked.`,
60    }
61  })
62}
63