SLOPSHOPPER

tt

Commands for discovering your unknowns across the map-vs-territory workflow (trq212, 'The map is not the territory'). Before: /blindspot, /brainstorm…

newbandtoasttimer
v0.0.169NOASSERTIONupdated 2026-10-26ChrisTowles/towles-tool/packages/core
A shopper browsing a rack in a slop shop
README

tt-core

Skills for discovering your unknowns — Claude Code helpers built around the map-vs-territory workflow from trq212, "The map is not the territory".

The map is what you give Claude (prompts, skills, context). The territory is where the work actually happens (the codebase, its real constraints). The gap between them is your unknowns, and reducing them is the skill of agentic coding. Each skill is a cheap way to find out what you didn't know — before it gets expensive to fix.

Commands

One command per technique, grouped by phase: before implementation, plan/during, after. Each is a generic prompt you invoke; pass an optional target as an argument.

Before implementation

CommandFinds…Description
/tt:blindspotunknown unknownsSurface what you don't know you don't know in an unfamiliar area, and teach it.
/tt:brainstormunknown knownsExplore approaches / prototype with fake data so you can react before wiring.
/tt:interviewknown unknownsInterview you one question at a time, architecture-changing questions first.
/tt:references—Convey intent with a reference (ideally source code); reimplement its semantics.

Plan & during implementation

CommandFinds…Description
/tt:plantweakable choicesPlan that leads with what you'll change (data models, types, UX), buries chores; then keeps implementation-notes with deviations while building.

After implementation

CommandDescription
/tt:pitchPackage the work into one buy-in doc, demo first.
/tt:comprehendReport on the change + a quiz you must pass before merging.
/tt:memoriesReview your memory files and recommend which to commit into CLAUDE.md.
/tt:handoffWrite a short restart prompt for a fresh agent, copied to your clipboard.

Landing the work

A ladder: each command is the one before it plus one step. The git and gh mechanics are scripts/pr.sh, which either finishes or stops with one STOP: reason and one FIX:; the agent only writes the words and picks which files belong.

CommandDescription
/tt:prBranch off the default branch if needed, commit, push, open the PR (or update the open one). Targets a stacked parent branch's PR when there is one.
/tt:prm/tt:pr, then wait for checks and merge with the method the repo allows. Running it permits an admin merge: one --admin retry when branch protection refuses. Checks must still pass.

/towles-tool-app:done (the app plugin) is the third rung: /tt:prm, then delete the task from towles-tool.

Mod: copy what Claude hands you

hooks/register.tsx is a Claude Code mod. When a turn ends with an answer, it offers each ! <command> Claude handed you to run in the prompt: a line, indented or inside a fence, that opens with ! , or an inline code span that does. Each keeps its ! , so a paste runs it in bash mode. SHAs, paths, links and identifiers are never offered, nor is a message with no such command; offers are deduplicated, at most three, with no model call.

1:  copy  ! gh pr merge 9 --squash --admin   a: copy all
2:  copy  ! gcloud auth login

The rows sit at the bottom of the band above the prompt, under anything another mod draws there (the towles-tool-app band). A digit in the empty prompt (or a click) copies that one; a, with the band focused, copies all of them in order, apart enough that a clipboard manager keeps each entry. The offer clears when the next turn starts. A session whose last three offers went uncopied is left alone until you copy one, with a single probe every 30 minutes.

hooks/clip.ts is the pure logic tests/ covers:

claude plugin validate packages/core
claude plugin test packages/core

Installation

In Claude Code 2.1.275 or later, one step:

/plugin install tt --marketplace ChrisTowles/towles-tool

Or from a shell:

claude plugin marketplace add ChrisTowles/towles-tool
claude plugin install tt@towles-tool

Restart Claude Code to load it. To pick up a new release:

claude plugin marketplace update towles-tool
claude plugin update tt@towles-tool
Source 3 files
hooks/register.tsx 88 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Clips } from '../types'
5import { COPY_ALL_GAP_MS, type Ignored, clipLine, clipShown, offers, quiet } from './clip'
6
7const clips = atom({ plugin: 'tt', key: 'clips' } as const, [] as Clips)
8
9/** Reset by a copy; a reload starts it over. */
10let ignored: Ignored = { offers: 0, lastMs: 0 }
11
12type Surface = Parameters<EngineInterface['ui']['copy']>[0]['surface']
13
14/** Several go one at a time, apart, so the clipboard manager keeps each in order. */
15async function copyOffers($: EngineInterface, texts: string[], surface: Surface) {
16  let copied = 0
17  for (const [i, text] of texts.entries()) {
18    if (i > 0) await new Promise<void>(done => $.clock.after(COPY_ALL_GAP_MS, done))
19    if ((await $.ui.copy({ text, surface })).isCopied) copied++
20  }
21  if (!copied) {
22    $.ui.toast('Nothing reached the clipboard.')
23    return
24  }
25  ignored = { offers: 0, lastMs: 0 }
26  $.ui.toast(texts.length === 1 ? `Copied ${clipShown(texts[0]!)}` : `Copied ${copied}, in order`)
27}
28
29export const register: Register = on => {
30  on('turn.start', async ($, e, next) => {
31    if (e.agentId === undefined) await update($, clips, () => [])
32    return next(e)
33  })
34
35  // Only the `! <command>`s Claude hands the user to run; nothing for any other message.
36  on('turn.complete', async ($, e, next) => {
37    const ran = await next(e)
38    if (e.agentId !== undefined || e.reason !== 'answer') return ran
39    const found = offers(e.answer)
40    const now = await $.clock.now()
41    if (!found.length || quiet(ignored, now)) return ran
42    ignored = { offers: ignored.offers + 1, lastMs: now }
43    await update($, clips, () => found)
44    return ran
45  })
46
47  // Below whatever else draws the band: one row per offer, a `copy` badge and the text.
48  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
49    const below = await next(e)
50    if (e.props.hasSurvey || e.props.isWorking) return below
51    const texts = (await read($, clips)).slice(0, e.props.maxRows - 1)
52    if (!texts.length) return below
53
54    const { Box, Text, Button } = $.ui.resolve(e)
55    return (
56      <Box flexDirection="column">
57        {below}
58        {texts.map((text, i) => (
59          <Box key={`row:${i}`} flexDirection="row" gap={1}>
60            <Button
61              key={`clip:${i}`}
62              hotkey={String(i + 1)}
63              plain
64              onPress={press => void copyOffers($, [text], press.surface)}
65            >
66              <Text backgroundColor="magenta" color="black">
67                {' copy '}
68              </Text>
69              <Text wrap="truncate-end"> {clipLine(text)}</Text>
70            </Button>
71            {i === 0 && texts.length > 1 && (
72              <Button
73                key="clip:all"
74                hotkey="a"
75                plain
76                dimColor
77                onPress={press => void copyOffers($, texts, press.surface)}
78              >
79                copy all
80              </Button>
81            )}
82          </Box>
83        ))}
84      </Box>
85    )
86  })
87}
88
hooks/clip.ts 57 lines
1const MAX_OFFERS = 3
2const SHOWN_CHARS = 40
3/** Offers a session may leave uncopied before it goes quiet. */
4export const IGNORED_LIMIT = 3
5/** How long a quiet session stays quiet before one probe offer, so a copy can still reset it. */
6export const QUIET_MS = 30 * 60 * 1000
7/** Apart enough that a clipboard manager records each write as its own entry. */
8export const COPY_ALL_GAP_MS = 400
9
10/** The command after a leading `! `, or `null` when `text` does not open with one. */
11function bang(text: string): string | null {
12  const command = text.trimStart().startsWith('! ') ? text.trimStart().slice(2).trim() : ''
13  return command || null
14}
15
16/** The `! <command>`s the answer hands the user, in order, deduplicated, at most three. A
17 * command is a line (indented, or inside a fence) that opens with `! `, or an inline code
18 * span that does; a trailing `\` continues it onto the next line. Each keeps its `! `, so a
19 * paste into the prompt runs it in bash mode. */
20export function offers(answer: string): string[] {
21  const found: string[] = []
22  const lines = answer.split('\n')
23  for (let i = 0; i < lines.length; i++) {
24    const line = lines[i]!.trim()
25    if (line.startsWith('```') || line.startsWith('~~~')) continue
26    let command = bang(line)
27    if (command) {
28      while (command.endsWith('\\') && i + 1 < lines.length) command += `\n${lines[++i]!.trimEnd()}`
29      found.push(command)
30    } else {
31      found.push(...line.split('`').filter((_, n) => n % 2 === 1).flatMap(span => bang(span) ?? []))
32    }
33  }
34  const out: string[] = []
35  for (const command of found) {
36    const text = `! ${command.trimEnd()}`
37    if (out.length < MAX_OFFERS && !out.includes(text)) out.push(text)
38  }
39  return out
40}
41
42/** The offer flattened to one band row. */
43export const clipLine = (offer: string): string => offer.trim().replace(/\s+/g, ' ')
44
45/** The same, cut short for the toast. */
46export function clipShown(offer: string): string {
47  const line = clipLine(offer)
48  return line.length > SHOWN_CHARS ? `${line.slice(0, SHOWN_CHARS - 1)}…` : line
49}
50
51/** Offers this session left uncopied, and when the last one went up. */
52export type Ignored = { offers: number; lastMs: number }
53
54export function quiet(ignored: Ignored, nowMs: number): boolean {
55  return ignored.offers >= IGNORED_LIMIT && nowMs - ignored.lastMs < QUIET_MS
56}
57
types/index.d.ts 12 lines
1/** The `! <command>`s a finished turn offers to copy, each with its bang. */
2export type Clips = string[]
3
4declare module 'claude-code' {
5  interface PluginState {
6    tt: {
7      /** What the last turn offered; cleared when the next turn starts. */
8      clips: Clips
9    }
10  }
11}
12