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

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.
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.
| Command | Finds… | Description |
|---|---|---|
/tt:blindspot | unknown unknowns | Surface what you don't know you don't know in an unfamiliar area, and teach it. |
/tt:brainstorm | unknown knowns | Explore approaches / prototype with fake data so you can react before wiring. |
/tt:interview | known unknowns | Interview you one question at a time, architecture-changing questions first. |
/tt:references | — | Convey intent with a reference (ideally source code); reimplement its semantics. |
| Command | Finds… | Description |
|---|---|---|
/tt:plan | tweakable choices | Plan that leads with what you'll change (data models, types, UX), buries chores; then keeps implementation-notes with deviations while building. |
| Command | Description |
|---|---|
/tt:pitch | Package the work into one buy-in doc, demo first. |
/tt:comprehend | Report on the change + a quiz you must pass before merging. |
/tt:memories | Review your memory files and recommend which to commit into CLAUDE.md. |
/tt:handoff | Write a short restart prompt for a fresh agent, copied to your clipboard. |
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.
| Command | Description |
|---|---|
/tt:pr | Branch 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.
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
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-toolhooks/register.tsx 88 lines1import { 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}
88hooks/clip.ts 57 lines1const 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}
57types/index.d.ts 12 lines1/** 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