SLOPSHOPPER

je-coupe

End-of-session ritual: typing 'je coupe' asks Claude to update the doc files and shows a band of the ones still untouched

newbandprompt
v0.1.0MITupdated 2026-10-05vincentlauriat/ModsTools/mods/je-coupe
A shopper browsing a rack in a slop shop
README

je-coupe

End-of-session ritual: typing "je coupe" asks Claude to bring every doc file of the project up to date, and tracks which ones are still untouched.

What it does

  • On prompt submit: if the prompt is "je coupe" (case, accents and punctuation ignored; text after it is allowed, e.g. "je coupe pour ce soir"), scans the session folder for COMMANDS.md, CHANGES.md, MEMORY.md, TODOS.md, PLAN.md, README.md, ARCHITECTURE.md, ARCHITECTURE_EN.md.
  • Rewrites the prompt by appending an instruction listing the docs found; those not modified since the session started are flagged (NOT modified this session).
  • Shows a band above the prompt (Je coupe: N docs not updated yet: …) with a Dismiss button. The band is re-evaluated after each main turn and disappears once every doc is newer than the session start.
  • Does nothing if none of the doc files exist in the folder.

Install

claude --plugin-dir /path/to/ModsTools/mods/je-coupe

Limits

  • Staleness is judged by file modification time against the session start: a doc touched by anything else counts as updated.
  • Only the exact trigger "je coupe" (optionally followed by more words) is recognised; the doc list is fixed.

Develop

claude plugin validate mods/je-coupe
claude plugin test mods/je-coupe   # 10 tests
Source 3 files
hooks/register.tsx 59 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Pending } from '../types'
5import { DOCS, bandLabel, instruction, isJeCoupe, staleDocs } from './ritual'
6import type { DocStat } from './ritual'
7
8const pending = atom({ plugin: 'je-coupe', key: 'pending' } as const, null)
9
10async function scan($: EngineInterface): Promise<DocStat[]> {
11  const cwd = await $.session.cwd()
12  const found: DocStat[] = []
13  for (const name of DOCS) {
14    try {
15      const stat = await $.fs.stat(`${cwd}/${name}`)
16      if (stat.kind === 'file') found.push({ name, mtimeMs: stat.mtimeMs })
17    } catch {
18      // missing doc: not part of this folder's ritual
19    }
20  }
21
22  return found
23}
24
25export const register: Register = on => {
26  on('prompt.submit', async ($, e, next) => {
27    if (!isJeCoupe(e.text)) return next(e)
28    const docs = await scan($)
29    if (docs.length === 0) return next(e)
30    const { startedAt } = await $.session.usage()
31    await update($, pending, () => ({ stale: staleDocs(docs, startedAt) }) satisfies Pending)
32
33    return next({ ...e, text: `${e.text}\n\n${instruction(docs, startedAt)}` })
34  })
35
36  on('turn.complete', async ($, e, next) => {
37    if (e.agentId === undefined && (await read($, pending)) !== null) {
38      const { startedAt } = await $.session.usage()
39      const stale = staleDocs(await scan($), startedAt)
40      await update($, pending, () => (stale.length === 0 ? null : { stale }))
41    }
42
43    return next(e)
44  })
45
46  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
47    const state = await read($, pending)
48    if (e.props.hasSurvey || state === null) return next(e)
49    const { Box, Button, Text } = $.ui.resolve(e)
50
51    return (
52      <Box>
53        <Text>{bandLabel(state.stale)}</Text>
54        <Button key="dismiss" label="Dismiss" onPress={() => update($, pending, () => null)} />
55      </Box>
56    )
57  })
58}
59
hooks/ritual.ts 38 lines
1export const DOCS = [
2  'COMMANDS.md',
3  'CHANGES.md',
4  'MEMORY.md',
5  'TODOS.md',
6  'PLAN.md',
7  'README.md',
8  'ARCHITECTURE.md',
9  'ARCHITECTURE_EN.md',
10] as const
11
12export type DocStat = { name: string; mtimeMs: number }
13
14export function isJeCoupe(text: string): boolean {
15  const norm = text
16    .normalize('NFD')
17    .replace(/\p{M}/gu, '')
18    .toLowerCase()
19    .replace(/[^a-z0-9]+/g, ' ')
20    .trim()
21
22  return norm === 'je coupe' || norm.startsWith('je coupe ')
23}
24
25export const staleDocs = (docs: DocStat[], startedAt: number) =>
26  docs.filter(d => d.mtimeMs < startedAt).map(d => d.name)
27
28export function instruction(docs: DocStat[], startedAt: number): string {
29  const stale = new Set(staleDocs(docs, startedAt))
30  const list = docs.map(d => (stale.has(d.name) ? `${d.name} (NOT modified this session)` : d.name)).join(', ')
31
32  return `End of session: before stopping, bring all these doc files up to date with the work done: ${list}.`
33}
34
35export function bandLabel(stale: string[]): string {
36  return `Je coupe: ${stale.length} doc${stale.length === 1 ? '' : 's'} not updated yet: ${stale.join(', ')}`
37}
38
types/index.d.ts 8 lines
1export type Pending = { stale: string[] }
2
3declare module 'claude-code' {
4  interface PluginState {
5    'je-coupe': { pending: Pending | null }
6  }
7}
8