SLOPSHOPPER

smart-compact

/smart-compact: write custom /compact instructions from the session, then compact with them

newcommandstatusmodeltimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · smart-compact
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /smart-compact ⎿ smart-compact: compacting with these instructions: ⎿ smart-compact: ⎿ smart-compact: OK ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ smart-compact: smart-compact: compacting
README

FloMasters Plumbing and Drains

Website prototype for FloMasters, Antonio Spence's one-person plumbing business in Hampton Roads, Virginia. It is a base design to show the owner: every fact he hasn't confirmed yet appears as an ideal-case placeholder with a "confirm this" tag, and every photo is an AI-generated stand-in to be replaced with a real one.

Run it

npm install
npm run build
npx astro preview

Open http://localhost:4321. Add ?tags=on to any page, or use "Items to confirm" in the bar at the top, to see what the owner needs to supply. /review lists every item as a printable checklist.

npm run build:public builds the site for launch. It refuses to run until every fact is confirmed and every sample review and stand-in photo is replaced.

Commands and checks: .claude/reference/commands.md.

Source 1 files
hooks/register.ts 86 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3// Asked of a fork of the session, which sees the whole transcript. Its answer is
4// passed to /compact as the custom instructions the summarizer follows.
5const REVIEW_PROMPT = `Review this whole conversation so far and write the custom instructions to pass to /compact. The summarizer reads them as directions for what to keep, so write imperatives to it.
6
7Keep, in priority order:
8
91. The user's goal for the session and any standing instructions or preferences they gave in it (style, scope limits, things to never do).
102. Current state: repo, branch, worktree path, PR URL, last commit SHA, deploy or preview URL, files created or changed.
113. Decisions made and the reason for each, including options the user rejected.
124. What is verified and how, and what is still unverified.
135. Open work: the next concrete step, pending questions to the user, background tasks still running.
146. Failed approaches and gotchas that would otherwise be retried.
157. Exact identifiers to keep verbatim: paths, commands, error strings, IDs, numbers.
16
17Tell the summarizer to drop resolved tangents, raw tool output, superseded plans, and anything re-readable from files or CLAUDE.md. Never invent a fact; if a state item is unknown, leave it out. Keep the block under about 300 words; cut from the bottom of the priority list first.
18
19Return exactly one fenced \`text\` block and nothing else. When the session's replies follow a named style or output style (caveman ultra, for example), make the first line \`Always use <that style>.\` so it survives the compaction; otherwise leave that line out.
20
21\`\`\`text
22Always use <reply style>.
23Goal: <one line>.
24Keep: <standing user instructions>.
25State: <branch, worktree, PR, SHA, files>.
26Decisions: <decision, why>; ...
27Verified: <what, how>. Unverified: <what>.
28Next: <next step>; open questions: <...>.
29Don't retry: <failed approach, cause>.
30Verbatim: <paths, commands, errors>.
31Drop tool output, resolved tangents, and superseded plans.
32\`\`\`
33
34Omit any line with nothing to say.`
35
36// The fork answers with one fenced `text` block; take its body.
37const fencedBody = (reply: string) => {
38  const match = reply.match(/```(?:text)?\r?\n([\s\S]*?)\r?\n```/)
39  return (match ? match[1] : reply).trim()
40}
41
42export const register: Register = on => {
43  on('session.start', async ($, e, next) => {
44    await $.command.register({
45      name: 'smart-compact',
46      description: 'Write custom /compact instructions from this session, then compact with them',
47    })
48    return next(e)
49  })
50
51  on('command.run', { command: 'smart-compact' }, async $ => {
52    $.ui.status('smart-compact: writing instructions')
53    const reply = await $.model.fork({ prompt: REVIEW_PROMPT })
54    if (!reply.isAnswered) {
55      $.ui.status(undefined)
56      return { text: `review failed (${reply.reason}); nothing compacted. Run /compact yourself.` }
57    }
58
59    const instructions = fencedBody(reply.text)
60    $.ui.status('smart-compact: compacting')
61    // The engine refuses to run a command from inside this hook, which still
62    // holds the command's turn; run it from a timer once the command finishes.
63    $.clock.after(500, () => void compactWhenIdle($, instructions, 10))
64
65    return { text: `compacting with these instructions:\n\n${instructions}` }
66  })
67}
68
69const compactWhenIdle = async ($: EngineInterface, instructions: string, triesLeft: number) => {
70  try {
71    // `$.session.compact` is refused in SDK sessions (the desktop app's Code
72    // tab); running `/compact` itself works in those and in the terminal.
73    await $.command.run({ command: 'compact', args: instructions })
74    $.ui.status(undefined)
75  } catch (err) {
76    // Retry only the refusal for a command run from inside a hook that still
77    // holds the turn; a cancelled or interrupted /compact stays cancelled.
78    if (triesLeft > 1 && /hook/i.test(String(err))) {
79      $.clock.after(1000, () => void compactWhenIdle($, instructions, triesLeft - 1))
80      return
81    }
82    $.ui.status(undefined)
83    $.ui.log(`smart-compact: compaction failed (${String(err)}). Paste the instructions above after /compact.`)
84  }
85}
86