Six exploration agents per project (premise-breaker, field-crosser, idea-sizer, and a lazy trogg for each), one ideas/ folder per run, and a guide in Claude's…

Part of my portfolio.
Mods for Claude Code that change what it does and what it shows. Each one is a hooks module: it runs inside Claude Code, hooks prompts, tool calls and turns, and draws a line above the prompt or a pane beside the transcript. The five here are deviation-kernel, cache-meter, goal-board, file-tree and idea-agents. All five ship with tests that run without a live session.
| Mod | What it does | Costs anything? |
|---|---|---|
| deviation-kernel | Keeps the assumptions your project refuses in Claude's system prompt on every request, and catches an edit that brings one back at the step where it lands. | One small Haiku call after each long answer (can be switched off) |
| cache-meter | Shows how long the prompt cache stays warm, what a miss would cost to rewrite, how full the context is, and the session's cost at API rates. | No |
| goal-board | Shows each session's goal, task progress and ETA, and /goals lists every running session on the machine. | No |
| file-tree | Shows the working directory as a file tree in a pane on the right, with folders you open and close and the files Claude just changed marked. | No |
| idea-agents | Gives a project six exploration agents (three that explore ideas, a lazy "trogg" for each) and one ideas/ folder per run, and tells Claude how to run a round. | Only when you run a round: the agents' own model use (the troggs run on Haiku) |
Install any one of them on its own.
Paste this into Claude Code, in a terminal session:
Set up mods from the GitHub repo Spendry/claude-code-mods for me. Work through these steps and stop where a step says to ask me.
1. Run `claude --version`. The mods need 2.1.287 or later; if it's older, tell me how to update and stop.
2. Show me the table of mods from the repo README (https://github.com/Spendry/claude-code-mods) and ask which ones I want. Don't pick for me.
3. Clone https://github.com/Spendry/claude-code-mods into a temporary folder. For each mod I chose, run `claude plugin validate <that folder>/<mod>`, read its `hooks/register.ts`, and tell me in plain words what it can touch (the `calls:` line lists it). Ask me whether to go ahead.
4. Run `claude plugin marketplace add Spendry/claude-code-mods`, then install each mod I approved with `claude plugin install <mod>@claude-code-mods --scope user`. A note that options are not yet set is expected; the defaults apply until I change them. Delete the temporary clone.
5. Run `claude plugin list` and confirm each one shows as enabled.
6. If I chose deviation-kernel: ask which project it's for, and interview me there for the assumptions that project refuses on purpose. Do not suggest refusals of your own; the point of the kernel is that they come from me. For each one I give you, help me word it, propose tells (words or /regex/ that would show up when it's broken) and an "instead:", then show me the KERNEL.md and write it to the project root only when I say so. Then ask which mode I want (ask, warn or block), explain the difference in one line each, and tell me to type `/kernel mode <mode>` in that project's session to set it.
7. If I chose cache-meter: ask whether I pay API rates or use a subscription, and explain that on a subscription its dollar figures are the API-equivalent, not my bill.
8. Finish with a short list of what I still have to do myself (from the "What only you can do" section of the README).
Claude runs the install. It can't decide your project's refusals or see the bands for you; that part stays yours.
KERNEL.md. Claude can help you word a refusal and suggest tells, but which assumptions to refuse is your design decision. Start with two or three. You can add more later with /kernel add.## Exempt in KERNEL.md (for example - ideas/**) so the guard skips them; code everywhere else stays checked./kernel log shows what tripped and what you decided./goals and /cache answer in text instead.cache-meter/hooks/register.ts with the date they were checked. A model not in the table is priced at the first row's rates and shown with ≈.At the prompt of a Claude Code terminal session, one line per mod:
/plugin install deviation-kernel --marketplace Spendry/claude-code-mods
/plugin install cache-meter --marketplace Spendry/claude-code-mods
/plugin install goal-board --marketplace Spendry/claude-code-mods
/plugin install file-tree --marketplace Spendry/claude-code-mods
/plugin install idea-agents --marketplace Spendry/claude-code-mods
Answer y to add the marketplace, then pick the user scope so the mod loads in every session. A mod with options shows a screen to set them. /plugin lists what's active.
To turn every mod off for one session: claude --safe-mode. To update: claude plugin update <mod>@claude-code-mods, then /reload-plugins.
Built and tested against Claude Code 2.1.296; needs 2.1.287 or later.
What it guarantees: the project's refused assumptions are in Claude's system prompt on every request, with nothing to paste and nothing lost to compaction. An edit that brings one back is caught at that step.
The problem it answers: a design that refuses a standard assumption tends to lose that refusal while the ordinary parts get filled in. Claude reaches for the conventional component in each slot, and the conventional component carries the assumption the design rejected.
Setup. Put a KERNEL.md at the project root (or .claude/KERNEL.md). /kernel init writes a template; KERNEL.example.md shows a filled-in one. Each refusal is one line:
- R1: one global clock paces every stage | tells: global clock, clock tree, /clk_?en/ | instead: each stage hands off by local request/acknowledge
In a folder with no kernel, the first prompt you type there asks whether Claude should help you write one: now, after that message, not this session, or never in that folder. Mid-project, Claude reads the project's own record first so its questions point at what you set out to do, then asks you for the refusals (it proposes none of its own) and writes KERNEL.md only when you say so. Your home folder is never asked. Turn the offer off with the offerKernel option.
What happens.
ask (default): holds the edit and asks you. Refusing sends Claude the refusal and its instead:.warn: lets it through and flags it to Claude with your next prompt.block: refuses the edit outright.drop in the band if a flag is wrong. Turn this off with the semanticCheck option if you don't want the extra calls.Commands. /kernel opens the kernel and recent trips · /kernel init · /kernel add <text> | tells: a, b | instead: ... · /kernel mode ask|warn|block · /kernel check (a full audit of the conversation so far, served mostly from cache) · /kernel log · /kernel reload.
Limits. Tells are matched as whole words or regexes, so a tell also fires on a line that rejects the assumption ("no central scheduler"). The semantic check tells the two apart; the keyword check does not. The kernel file is never edited by Claude unless you ask.
What it guarantees: you can see how long the cached prefix has left, what a miss would cost to rewrite, context fill and session cost, without asking.
cache ▰▰▰▰▱▱▱▱ 2:41 · miss $0.50 · ctx 84k/200k 42% · session $4.10 API
/cache gives the full breakdown, including the hit and miss price of the next request.cacheTtl option defaults to auto. It assumes the 5-minute cache until a request reads from cache after more than five idle minutes, which only the 1-hour cache can serve. It then switches to 1h, toasts once, and remembers that for later sessions. The rewrite price changes with it (1.25x input for 5m, 2x for 1h). Set 5m or 1h to fix it.hooks/register.ts. On a subscription the dollar figures are what the same work would cost at API rates, not what you're billed.What it guarantees: each session shows its goal, task progress and ETA, and one command shows every running session on this machine.
/goal <text>. /goal clear resets it./goals opens the board of all sessions: working, idle or done, the current task, and the ETA.What it guarantees: you can see the project's files while Claude works on them, and which ones it has changed this session.
╭──────────────────────────────────────────╮
│ work ‹ up home refresh collapse │
│ ──────────────────────────────────────── │
│ ├─ ▸ notes │
│ └─ ▾ relay · here │
│ ├─ ▾ src │
│ │ ├─ ● main.ts │
│ │ └─ util.ts │
│ └─ README.md │
│ │
│ 1 changed · 1 up from relay │
╰──────────────────────────────────────────╯
/files opens it (above the prompt on the main screen). /files again closes it.r refreshes and c collapses every folder.‹ up (or u) moves the tree's root up one folder per press, like cd .., until it reaches the drive or filesystem root. The working folder stays open under its parent and is marked · here. home (or h) puts the root back on the working folder. Only the view moves; Claude's working directory stays where it was.background is the hex color painted behind the tree; the default #0f0f0f is half of Windows Terminal's Dark+ background. Set it to half of your own terminal's background, or leave it empty to keep the terminal's. coverFrame (on by default) draws the orange border over Claude Code's own square pane frame, so the side facing the conversation is orange; Claude Code still shows its thin grey line along the top. Turn it off to draw the border just inside the frame instead.…: a file or folder name keeps its start, the path in the footer keeps its end.●. A folder holding one has its name in green, so a change inside a closed folder still shows.duel option..git, node_modules, __pycache__, .venv, venv and .next are left out. A tree is cut at 1,500 rows; close a folder to see the rest.What it guarantees: a project can get a second set of eyes on a stuck problem, from agents that propose and size ideas but never touch its code, with every run kept in its own folder you can read later.
premise-breaker lists the assumptions under a problem and flips each one. field-crosser brings mechanisms from other fields, at least half outside computing. idea-sizer checks the ideas against numbers the project already measured and gives each a kill test. Each has a lazy helper on Haiku, a "trogg": Skip asks "why do it at all?", Borrow "what already does this?", Poke "what's the dumbest quick try?". The upper agents read their trogg's note and say in one line what they took..claude/agents/. It never overwrites an agent the project already has. Then it asks whether Claude should tailor them to the project (its building blocks, gates, results files); Claude shows each change and writes it only when you say so. Home folders and folders you said never for are skipped./ideas new <topic> makes ideas/<date>-<nn>-<topic>/ (or under research/ideas/ if the project keeps research apart, or .claude/agents/ideas/ if that folder exists, to keep runs beside the agents) with a run card (README.md: question, read order, outcome, decisions) and 0-data.md to fill. The agents write 1-premises.md, 2-fields.md, 3-sizing.md and their lazy notes next to it; the main session writes 4-result.md./ideas shows what's installed and the latest run. /ideas install adds missing agents. /ideas tailor starts the tailoring.Each mod is a folder with .claude-plugin/plugin.json, hooks/hooks.json, hooks/register.ts (or .tsx for a mod that draws a pane) and tests/. The marketplace file at .claude-plugin/marketplace.json lists them.
claude plugin validate <mod folder> reports what the mod hooks and calls, and anything the engine would refuse.claude plugin test <mod folder> runs its tests without a session.claude plugin marketplace add <path to clone>) and install from it. A folder marketplace is read from the folder itself, so after an edit /reload-plugins picks up the change with no reinstall.A new mod is a new folder plus one entry in marketplace.json.
MIT. See LICENSE.
hooks/register.ts 215 lines1import type { Register } from 'claude-code'
2
3// Six agents: three that explore, and a lazy helper (a trogg) for each.
4const AGENTS = ['premise-breaker', 'field-crosser', 'idea-sizer', 'trogg-skip', 'trogg-borrow', 'trogg-poke']
5const AGENT_DIR = '.claude/agents'
6// The ideas folder: .claude/agents/ideas/ where a project keeps its runs beside the agents,
7// research/ideas/ in a project that keeps research apart, else ideas/.
8const IDEA_DIRS = ['.claude/agents/ideas', 'research/ideas', 'ideas']
9
10const OFFER = { yes: 'Install them', later: 'Not this session', never: 'Never in this folder' }
11const TAILOR = { now: 'Yes, tailor them now', later: 'Later', keep: 'Keep them generic' }
12
13// The guide Claude carries in a project that has the agents.
14const GUIDE = `<idea_agents>
15This project has six exploration agents in .claude/agents/: premise-breaker, field-crosser and idea-sizer, each with a lazy helper (trogg-skip, trogg-borrow, trogg-poke, on haiku). They propose and size ideas; they never edit project code. Use them when the person asks for a round on a problem, or when a goal is close but stuck and the person agrees to a round.
16
17A round, one folder per run:
181. \`/ideas new <topic>\` makes <ideas>/<date>-<nn>-<topic>/ (the ideas folder is .claude/agents/ideas/ where the project has it, research/ideas/ where the project keeps research apart, else ideas/) with a run card (README.md) and 0-data.md. Run files carry no frontmatter: Claude Code reads .md files under .claude/agents/ as agents. Fill 0-data.md first: the question, the current numbers with their sources, the rules (gates, KERNEL.md refusals), and which parts of the current method are choices rather than facts.
192. Start premise-breaker and field-crosser together, each told the run folder. When each finishes, run its trogg on its file (trogg-skip on 1-premises.md, trogg-borrow on 2-fields.md). Subagents can't start subagents, so the main session runs every trogg.
203. Send each upper agent its trogg's note and ask for the short revise step (same agent, so it keeps its context).
214. Start idea-sizer on both files; give it a scratch folder for probes. Then trogg-poke on 3-sizing.md.
225. Report the try-next list to the person. Run only the kill tests they pick. Write 4-result.md (what was run, the numbers, PASS or FAIL against each kill bar) and finish the run card: question, read order, outcome, decisions.
23
24Hold to these:
25- A ceiling is only as good as the premise it holds fixed. Say what a poke holds fixed.
26- A gate's bar never moves. An idea that changes a gate's terms needs the person's yes before it runs.
27- Never score held-out or gate data while choosing; size on validation, then run the gate once.
28- Agent reports are model output. Check a claim against the file or number before passing it on.
29- New agent files load at the next session start. Until then, run each through the general-purpose agent, told to read its file under .claude/agents/ and follow it exactly (haiku for the troggs).
30- If a guard blocks an agent's note, stop and ask the person; don't route around it.
31</idea_agents>`
32
33function folderKey(path: string): string {
34 return path.replaceAll('\\', '/').replace(/\/+$/, '').toLowerCase()
35}
36
37// Paths hang off the session's root folder: the working directory can move during a session.
38async function at($: any, path: string): Promise<string> {
39 return `${(await $.session.root()).replace(/[\\/]+$/, '')}/${path}`
40}
41
42async function ideasDir($: any): Promise<string> {
43 for (const d of IDEA_DIRS) if (await $.fs.exists(await at($, d))) return d
44 return (await $.fs.exists(await at($, 'research'))) ? 'research/ideas' : 'ideas'
45}
46
47async function missing($: any): Promise<string[]> {
48 const out: string[] = []
49 for (const a of AGENTS) if (!(await $.fs.exists(await at($, `${AGENT_DIR}/${a}.md`)))) out.push(a)
50 return out
51}
52
53/** Writes the agents this project lacks (never overwrites one) and ideas/README.md if absent. */
54async function install($: any): Promise<string[]> {
55 const wrote: string[] = []
56 for (const a of await missing($)) {
57 await $.fs.write(await at($, `${AGENT_DIR}/${a}.md`), await $.fs.read(`${$.plugin.root}/templates/${a}.md`))
58 wrote.push(a)
59 }
60 const IDEAS = await ideasDir($)
61 if (!(await $.fs.exists(await at($, `${IDEAS}/README.md`))))
62 await $.fs.write(await at($, `${IDEAS}/README.md`), await $.fs.read(`${$.plugin.root}/templates/ideas-README.md`))
63 return wrote
64}
65
66function slug(topic: string): string {
67 return topic.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'run'
68}
69
70/** ideas/<date>-<nn>-<topic>/ with a run card and a data note to fill. */
71async function newRun($: any, topic: string): Promise<string> {
72 const date = new Date(await $.clock.now()).toISOString().slice(0, 10)
73 const IDEAS = await ideasDir($)
74 let entries: { name: string; kind: string }[] = []
75 try {
76 entries = await $.fs.list(await at($, IDEAS))
77 } catch {
78 // no ideas folder yet
79 }
80 const today = entries.filter(e => e.kind === 'directory' && e.name.startsWith(`${date}-`)).length
81 const name = `${date}-${String(today + 1).padStart(2, '0')}-${slug(topic)}`
82 const dir = `${IDEAS}/${name}`
83 const full = await at($, dir)
84 await $.fs.write(
85 `${full}/README.md`,
86 `# Run ${String(today + 1).padStart(2, '0')} · ${topic}\n\n**Question.** \n\n**Outcome.** (open)\n\n## Read in this order\n| File | Agent | What it holds |\n|---|---|---|\n| \`0-data.md\` | main session | |\n| \`1-premises.md\` | premise-breaker | |\n| \`1-premises-lazy-skip.md\` | trogg-skip | |\n| \`2-fields.md\` | field-crosser | |\n| \`2-fields-lazy-borrow.md\` | trogg-borrow | |\n| \`3-sizing.md\` | idea-sizer | |\n| \`3-sizing-lazy-poke.md\` | trogg-poke | |\n| \`4-result.md\` | main session | |\n\n## Decisions\n`,
87 )
88 await $.fs.write(
89 `${full}/0-data.md`,
90 `# ${topic}: the data\n\n## The question\n\n## Current numbers (with sources)\n\n## Rules (gates, refusals, what may not change)\n\n## Choices in the current method, not facts\n\n## Code and files\n`,
91 )
92 return dir
93}
94
95const TAILOR_NOTE = `[idea-agents] The person asked you to tailor the six exploration agents in .claude/agents/ to this project. Read the project's own record first (CLAUDE.md, README, design notes, KERNEL.md, where results are kept). Then, one agent at a time, propose specific edits: the project's real building blocks in place of generic ones, its gates and units, its results files, its scratch and probe limits. Show each change and write it only when they say so. Keep the six roles and the run-folder file names as they are.`
96
97// ---------- module state (rebuilt on every reload) ----------
98let offer = true
99let installed = false
100// Folders already offered this session. A reload asks again; /clear does not.
101const offered = new Set<string>()
102
103async function refresh($: any) {
104 installed = (await missing($)).length === 0
105}
106
107export const register: Register = (on, options) => {
108 offer = ((options as { offer?: boolean }) ?? {}).offer !== false
109
110 on('session.start', async ($, e, next) => {
111 await refresh($)
112 try {
113 await $.command.register({
114 name: 'ideas',
115 description: 'Exploration agents: status, install, new <topic>, tailor',
116 argumentHint: '[install | new <topic> | tailor]',
117 immediate: true,
118 })
119 } catch {
120 // name taken: the rest of the mod still works
121 }
122 return next(e)
123 })
124
125 // The guide rides in the system prompt only where the agents are installed.
126 on('prompt.compose', async ($, e, next) => {
127 const composed = await next(e)
128 if (!installed) return composed
129 return { sections: [...composed.sections, { id: 'idea-agents:guide', text: GUIDE, scope: 'session' as const }] }
130 })
131
132 // First typed prompt in a folder without the agents: offer them, once.
133 on('prompt.submit', async ($, e, next) => {
134 await refresh($)
135 if (installed || !offer || e.command || e.isMeta || (e.origin.kind !== 'composer' && e.origin.kind !== 'bridge')) return next(e)
136 const cwd = await $.session.root()
137 const key = folderKey(cwd)
138 if (offered.has(key)) return next(e)
139 offered.add(key)
140 const homes = [await $.env.get('HOME'), await $.env.get('USERPROFILE')].filter(Boolean).map((h: string) => folderKey(h))
141 if (homes.includes(key) || (await $.store.get(`never:${key}`))) return next(e)
142
143 const lacking = await missing($)
144 const folder = cwd.split(/[\\/]/).filter(Boolean).pop() ?? cwd
145 let answer = OFFER.later
146 try {
147 answer = await $.ui.ask(
148 `${folder} has ${lacking.length === AGENTS.length ? 'no' : `only ${AGENTS.length - lacking.length} of the six`} exploration agents (three that explore ideas, three lazy helpers). Install ${lacking.length === AGENTS.length ? 'them' : 'the missing ones'} into .claude/agents/?`,
149 { header: 'Idea agents', options: [OFFER.yes, OFFER.later, OFFER.never] },
150 )
151 } catch {
152 // dismissed, or nobody to ask
153 }
154 if (answer === OFFER.never) {
155 await $.store.set(`never:${key}`, true)
156 $.ui.toast(`idea-agents: no more offers in ${folder}. /ideas install still works.`)
157 return next(e)
158 }
159 if (answer !== OFFER.yes) return next(e)
160
161 const wrote = await install($)
162 await refresh($)
163 let tailor = TAILOR.later
164 try {
165 tailor = await $.ui.ask('Installed. Should Claude tailor the agents to this project (its building blocks, gates, results files)?', {
166 header: 'Idea agents',
167 options: [TAILOR.now, TAILOR.later, TAILOR.keep],
168 })
169 } catch {
170 // dismissed: leave them generic for now
171 }
172 const notes = [
173 `[idea-agents] Installed ${wrote.join(', ')} into .claude/agents/ and the ideas folder's README.md if it was missing. They load as agent types at the next session start; until then run them through the general-purpose agent told to follow each file. Mention this in one line, then answer the person's message.`,
174 ]
175 if (tailor === TAILOR.now) notes.push(TAILOR_NOTE + ' Do this after answering their message.')
176 return next({ ...e, context: [...(e.context ?? []), ...notes] })
177 }).catch(($, e, next) => next(e)) // a failed offer never holds up the prompt
178
179 on('command.run', { command: 'ideas' }, async ($, e) => {
180 const [sub, ...rest] = (e.args ?? '').trim().split(/\s+/)
181 const arg = rest.join(' ').trim()
182
183 if (sub === 'install') {
184 const wrote = await install($)
185 await refresh($)
186 return { text: wrote.length ? `Installed ${wrote.join(', ')} into ${AGENT_DIR}/. They load as agent types at the next session start.` : 'All six agents are already here.' }
187 }
188
189 if (sub === 'new') {
190 if (!arg) return { text: 'Usage: /ideas new <topic>' }
191 const dir = await newRun($, arg)
192 return { text: `Made ${dir}/ with README.md (the run card) and 0-data.md. Fill 0-data.md, then ask Claude to run the round.` }
193 }
194
195 if (sub === 'tailor') {
196 if (!installed) return { text: 'The agents are not all installed here. Run /ideas install first.' }
197 const r = await $.model.fork({ prompt: TAILOR_NOTE + ' Start with the first agent now.' })
198 return { text: r.isAnswered ? r.text : 'Claude could not start the tailoring just now.' }
199 }
200
201 // bare /ideas: status
202 const lacking = await missing($)
203 const IDEAS = await ideasDir($)
204 let runs: string[] = []
205 try {
206 runs = (await $.fs.list(await at($, IDEAS))).filter((x: { kind: string }) => x.kind === 'directory').map((x: { name: string }) => x.name).sort()
207 } catch {
208 // no ideas folder
209 }
210 const agents = lacking.length ? `Missing: ${lacking.join(', ')}. /ideas install adds them.` : 'All six agents installed.'
211 const last = runs.length ? `\n${runs.length} run(s) in ${IDEAS}/, latest ${runs[runs.length - 1]}.` : `\nNo runs yet. /ideas new <topic> starts one.`
212 return { text: agents + last }
213 })
214}
215