Claude logs each assumption it makes while working; review them in a side panel, mark them right or wrong, and send corrections mid-task.

Claude logs each assumption it makes while it works. You review them, mark them right or wrong, and send a correction in the middle of a task.
log_assumption. Claude calls it when it makes a non-trivial assumption you have not confirmed. It can mark an assumption as high impact, and it carries on without waiting./assumptions shows what Claude assumed in this session. A side panel shows the same list.Correction to assumption A<n> ("…"): <your fix> and asks Claude to adjust the work that depended on it.session-board, its recap shows how many assumptions are still open. This mod writes the count to a small file in ~/.claude/session-board/. Nothing reaches the model./plugin marketplace add richardmuucyi/rmahwj-mods
/plugin install assumptions@rmahwj-mods
/reload-plugins
claude plugin test assumptionshooks/register.tsx 428 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Assumption } from '../types'
5
6const PANE = 'assumptions'
7const TOOL = 'mcp__assumptions__log_assumption'
8
9const list = atom({ plugin: 'assumptions', key: 'list' } as const, [])
10const editing = atom({ plugin: 'assumptions', key: 'editing' } as const, null)
11const isWorking = atom({ plugin: 'assumptions', key: 'isWorking' } as const, false)
12const nextId = atom({ plugin: 'assumptions', key: 'nextId' } as const, 1)
13const showReviewed = atom({ plugin: 'assumptions', key: 'showReviewed' } as const, false)
14
15// After the last open assumption is reviewed, the panel closes after this pause.
16const CLOSE_DELAY_MS = 2_000
17
18const GUIDANCE = `## Logging assumptions
19While you work, whenever you make a non-trivial assumption the user has not confirmed (about requirements, scope, file or folder locations, formats, naming, defaults, data, or which option they want), call the tool \`${TOOL}\` at once with that assumption in one short sentence, and a few words on why. Then keep working; do not stop to ask. The user reviews the assumptions as they come.
20- Set impact to "high" when a wrong guess would be costly to undo (deleting or overwriting data, architecture, money, anything sent to other people); otherwise leave it out.
21- Log one assumption per call, when you make it, not in a batch at the end.
22- Skip trivial or obvious choices, and anything the user already stated.
23- If a message arrives that says "Correction to assumption A<n>", follow it: revise the work that depended on that assumption, and say briefly what you changed.`
24
25function short(text: string, n = 60): string {
26 const one = text.replace(/\s+/g, ' ').trim()
27 return one.length > n ? `${one.slice(0, n - 1)}…` : one
28}
29
30function ago(ms: number): string {
31 const s = Math.max(0, Math.round(ms / 1000))
32 if (s < 60) return `${s}s ago`
33 const m = Math.round(s / 60)
34 return m < 60 ? `${m}m ago` : `${Math.round(m / 60)}h ago`
35}
36
37/** True when a drawing shows nothing (an empty box or blank text): such a tree only adds a blank row. */
38function isBlank(n: unknown): boolean {
39 if (n === null || n === undefined || n === false || n === true) return true
40 if (typeof n === 'string') return n.trim() === ''
41 if (typeof n === 'number') return false
42 if (Array.isArray(n)) return n.every(isBlank)
43 const el = n as { type?: unknown; children?: unknown[] }
44 if (el.type === 'Box' || el.type === 'Text') return (el.children ?? []).every(isBlank)
45 return false
46}
47
48/** Review order: high impact first, skipped ones last, then oldest first. */
49function queue(items: Assumption[]): Assumption[] {
50 return items
51 .filter(a => a.status === 'open')
52 .sort(
53 (x, y) =>
54 Number(y.impact === 'high') - Number(x.impact === 'high') ||
55 (x.skippedAt ?? 0) - (y.skippedAt ?? 0) ||
56 x.id - y.id,
57 )
58}
59
60/** Mark one assumption reviewed; close the panel shortly after the last one. */
61async function mark($: EngineInterface, id: number, status: Assumption['status'], correction?: string): Promise<void> {
62 await update($, list, all => all.map(a => (a.id === id ? { ...a, status, correction } : a)))
63 await closeIfDone($)
64}
65
66/** Close the panel once nothing is left open: a short pause first so the last ✓ or ✗ is seen. */
67async function closeIfDone($: EngineInterface): Promise<void> {
68 if (queue(await read($, list)).length > 0) return
69 $.clock.after(CLOSE_DELAY_MS, () => {
70 void (async () => {
71 const still = queue(await read($, list)).length
72 if (still === 0 && (await read($, editing)) === null) await $.ui.close({ id: PANE })
73 })()
74 })
75}
76
77/** Send a correction to Claude: into the running turn if one runs, else as a new prompt. */
78async function sendCorrection($: EngineInterface, a: Assumption, value: string): Promise<void> {
79 const fix = value.trim()
80 if (fix === '') return
81 await update($, editing, () => null)
82 await mark($, a.id, 'wrong', fix)
83 const text = `Correction to assumption A${a.id} ("${a.text}"): ${fix}\nPlease adjust the work that depended on it.`
84 if (await read($, isWorking)) {
85 // Mid-turn: Claude reads it at its next step.
86 await $.session.append({ message: { type: 'user', content: [{ type: 'text', text }] } })
87 $.ui.toast(`Correction to A${a.id} sent to Claude`)
88 } else {
89 await $.prompt.submit({ text, asUser: true })
90 }
91}
92
93/** Start a correction: a field where the surface has one, else the prompt box pre-filled. */
94async function startFix($: EngineInterface, a: Assumption, hasInput: boolean): Promise<void> {
95 if (hasInput) {
96 await update($, editing, () => a.id)
97 return
98 }
99 await mark($, a.id, 'wrong')
100 await $.prompt.fill({ text: `Correction to assumption A${a.id} ("${a.text}"): `, mode: 'replace' })
101}
102
103export const register: Register = on => {
104 on('session.start', async ($, e, next) => {
105 await $.tool.register({
106 name: 'log_assumption',
107 description:
108 'Record one assumption you are making while working, so the user can confirm or correct it. Call it the moment you make a non-trivial assumption the user has not confirmed, then carry on.',
109 inputSchema: {
110 type: 'object',
111 properties: {
112 assumption: { type: 'string', description: 'The assumption, in one short sentence.' },
113 why: { type: 'string', description: 'Optional: a few words on why you assumed it.' },
114 impact: {
115 type: 'string',
116 enum: ['high', 'low'],
117 description: '"high" when a wrong guess would be costly to undo; omit otherwise.',
118 },
119 },
120 required: ['assumption'],
121 },
122 isDeferred: false,
123 })
124 await $.command.register({
125 name: 'assumptions',
126 description: 'Show the assumptions Claude made in this session',
127 })
128 // How many are still open, for session-board's recap chip: a mod cannot
129 // read another mod's state, so it goes in a small file beside the board's
130 // cards. Written only when the count changes; nothing reaches the model.
131 const home = ((await $.env.get('USERPROFILE').catch(() => undefined)) || (await $.env.get('HOME').catch(() => undefined)) || '').replace(/[\\/]$/, '')
132 if (home !== '') {
133 let last = ''
134 $.clock.every(2_000, () => {
135 void (async () => {
136 const all = await read($, list)
137 const open = all.filter(a => a.status === 'open')
138 const text = JSON.stringify({ open: open.length, high: open.filter(a => a.impact === 'high').length })
139 // The id changes on /clear: the count follows the session it belongs to.
140 const path = `${home}/.claude/session-board/${await $.session.id()}.assume`
141 if (`${path}|${text}` === last) return
142 last = `${path}|${text}`
143 await $.fs.write(path, text)
144 })().catch(() => undefined)
145 })
146 }
147 return next(e)
148 })
149
150 // Tell the model to log its assumptions.
151 on('prompt.compose', async ($, e, next) => {
152 const composed = await next(e)
153 return {
154 sections: [...composed.sections, { id: 'assumptions:guidance', text: GUIDANCE, scope: 'session' as const }],
155 }
156 })
157
158 // The tool itself: store the assumption and answer at once.
159 on('tool.call', { tool: TOOL }, async ($, e) => {
160 // Arguments sit beside `tool` on the event (older callers nest them under `input`).
161 const raw = e as unknown as Record<string, unknown>
162 const input = (
163 typeof raw.assumption === 'string' ? raw : ((raw.input as Record<string, unknown> | undefined) ?? {})
164 ) as { assumption?: unknown; why?: unknown; impact?: unknown }
165 const text = typeof input.assumption === 'string' ? input.assumption.trim() : ''
166 if (text === '') return { deny: 'assumption is required' }
167 const why = typeof input.why === 'string' && input.why.trim() !== '' ? input.why.trim() : undefined
168 const impact = input.impact === 'high' ? ('high' as const) : ('low' as const)
169
170 let id = 0
171 await update($, nextId, n => {
172 id = n
173 return n + 1
174 })
175 const one: Assumption = { id, text, why, impact, status: 'open', at: await $.clock.now() }
176 await update($, list, items => [...items, one])
177 return { result: `Logged as A${id}. Carry on; the user will send a correction if it is wrong.` }
178 })
179
180 on('turn.start', async ($, e, next) => {
181 await update($, isWorking, () => true)
182 return next(e)
183 })
184
185 on('turn.complete', async ($, e, next) => {
186 await update($, isWorking, () => false)
187 return next(e)
188 })
189
190 on('command.run', { command: 'assumptions' }, async $ => {
191 const opened = await $.ui.open({ id: PANE, title: 'Assumptions', closeOnEscape: true })
192 const items = await read($, list)
193 if (items.length === 0) return { text: 'No assumptions logged yet in this session.' }
194 return {
195 text: opened.isPlaced ? `Assumptions panel opened (${items.length} logged).` : 'Widen the window to see the assumptions panel.',
196 }
197 })
198
199 // The card above the prompt: one assumption at a time, answered in place.
200 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
201 const items = await read($, list)
202 const open = queue(items)
203 if (open.length === 0 || e.props.hasSurvey) return next(e)
204 const els = $.ui.resolve(e)
205 const { Box, Button, Text } = els
206 const Input = 'Input' in els ? els.Input : undefined
207 const cur = open[0]!
208 const hot = cur.impact === 'high'
209 const edit = await read($, editing)
210 const t = await $.clock.now()
211 const isFixing = edit === cur.id && Input !== undefined
212 const accent = hot ? 'yellow' : 'gray'
213
214 const header = (
215 <Box key="as-card-head">
216 <Text bold color="black" backgroundColor={accent}>
217 {` ${hot ? '⚠ ' : ''}${isFixing ? 'FIXING' : 'ASSUMPTION'} 1/${open.length} `}
218 </Text>
219 <Text dimColor>
220 {' '}
221 {hot ? 'high impact · ' : ''}
222 {cur.at !== undefined ? ago(t - cur.at) : `A${cur.id}`}
223 </Text>
224 </Box>
225 )
226
227 const body = isFixing ? (
228 <Box key="as-card-fixing" flexDirection="column">
229 <Text dimColor>{cur.text}</Text>
230 <Input
231 key="as-card-fix-input"
232 label="It should be: "
233 placeholder="tell Claude what's right"
234 submitLabel="send"
235 autoFocus
236 onSubmit={(value: string) => sendCorrection($, cur, value)}
237 />
238 <Box>
239 <Button key="as-card-cancel" label="Cancel" onPress={() => update($, editing, () => null)} />
240 </Box>
241 </Box>
242 ) : (
243 <Box key="as-card-body" flexDirection="column">
244 <Text bold>{cur.text}</Text>
245 {cur.why !== undefined && (
246 <Text dimColor italic>
247 why: {cur.why}
248 </Text>
249 )}
250 <Box marginTop={1}>
251 <Button key="as-card-right" label="✓ Right" hotkey="y" variant="primary" onPress={() => mark($, cur.id, 'right')} />
252 <Text> </Text>
253 <Button key="as-card-fix" label="✗ Fix" hotkey="n" onPress={() => startFix($, cur, Input !== undefined)} />
254 <Text> </Text>
255 <Button
256 key="as-card-skip"
257 label="Skip"
258 hotkey="s"
259 onPress={async () => {
260 const now = await $.clock.now()
261 await update($, list, all => all.map(a => (a.id === cur.id ? { ...a, skippedAt: now } : a)))
262 }}
263 />
264 <Text> </Text>
265 <Button
266 key="as-open"
267 plain
268 onPress={() => $.ui.open({ id: PANE, title: 'Assumptions', closeOnEscape: true })}
269 >
270 <Text color="blue">all {open.length} ›</Text>
271 </Button>
272 </Box>
273 </Box>
274 )
275
276 const mine = (
277 <Box key="as-card" flexDirection="column" borderStyle="round" borderColor={accent} paddingX={1}>
278 {header}
279 {body}
280 </Box>
281 )
282 // Keep whatever else is drawn above the prompt (other mods' strips) beneath this one.
283 const beneath = await next(e)
284 return (
285 <Box flexDirection="column">
286 {mine}
287 {!isBlank(beneath) && beneath}
288 </Box>
289 )
290 })
291
292 // The panel: a timeline of every assumption, to review in bulk or look back.
293 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
294 const els = $.ui.resolve(e)
295 const { Box, Button, Text } = els
296 // Mobile draws no text field: there, Fix pre-fills the prompt instead.
297 const Input = 'Input' in els ? els.Input : undefined
298 const items = await read($, list)
299 const edit = await read($, editing)
300
301 if (items.length === 0) {
302 return (
303 <Box flexDirection="column">
304 <Text dimColor>No assumptions logged yet.</Text>
305 <Text dimColor>Claude adds one here each time it assumes something while working.</Text>
306 </Box>
307 )
308 }
309
310 const open = queue(items)
311 const reviewed = items.filter(a => a.status !== 'open').reverse()
312 const showAll = await read($, showReviewed)
313
314 const openRow = (a: Assumption) => {
315 const hot = a.impact === 'high'
316 return (
317 <Box key={`as-row-${a.id}`} flexDirection="column" marginTop={1}>
318 <Text>
319 <Text color={hot ? 'yellow' : undefined}>● </Text>
320 <Text bold>
321 A{a.id} {a.text}
322 </Text>
323 {hot && <Text color="yellow"> ⚠ high</Text>}
324 </Text>
325 {a.why !== undefined && (
326 <Text>
327 <Text dimColor>│ </Text>
328 <Text dimColor italic>
329 why: {a.why}
330 </Text>
331 </Text>
332 )}
333 {edit === a.id && Input !== undefined ? (
334 <Box key={`as-fixing-${a.id}`} flexDirection="column">
335 <Input
336 key={`as-fix-${a.id}`}
337 label="│ It should be: "
338 placeholder="tell Claude what's right, then Enter"
339 submitLabel="send"
340 autoFocus
341 onSubmit={(value: string) => sendCorrection($, a, value)}
342 />
343 <Box>
344 <Text dimColor>│ </Text>
345 <Button key={`as-cancel-${a.id}`} label="Cancel" onPress={() => update($, editing, () => null)} />
346 </Box>
347 </Box>
348 ) : (
349 <Box key={`as-acts-${a.id}`}>
350 <Text dimColor>│ </Text>
351 <Button key={`as-right-${a.id}`} label="✓ Right" variant="primary" onPress={() => mark($, a.id, 'right')} />
352 <Text> </Text>
353 <Button key={`as-wrong-${a.id}`} label="✗ Fix…" onPress={() => startFix($, a, Input !== undefined)} />
354 </Box>
355 )}
356 </Box>
357 )
358 }
359
360 const reviewedRow = (a: Assumption) => (
361 <Box key={`as-row-${a.id}`} flexDirection="column">
362 <Text>
363 <Text color={a.status === 'right' ? 'green' : 'red'}>● </Text>
364 <Text dimColor>
365 A{a.id} {a.text}
366 </Text>
367 <Text color={a.status === 'right' ? 'green' : 'red'}>{a.status === 'right' ? ' ✓ right' : ' ✗ fixed'}</Text>
368 </Text>
369 {a.correction !== undefined && (
370 <Text>
371 <Text dimColor>│ </Text>
372 <Text color="green">→ {a.correction}</Text>
373 </Text>
374 )}
375 </Box>
376 )
377
378 return (
379 <Box flexDirection="column">
380 <Text>
381 <Text bold color="black" backgroundColor={open.length > 0 ? 'yellow' : 'gray'}>
382 {` TO REVIEW ${open.length} `}
383 </Text>
384 </Text>
385 {open.length === 0 && <Text dimColor>All reviewed.</Text>}
386 {open.map(openRow)}
387 {reviewed.length > 0 && (
388 <Box marginTop={1}>
389 <Button key="as-toggle-reviewed" plain onPress={() => update($, showReviewed, v => !v)}>
390 <Text bold color="white" backgroundColor="gray">
391 {` REVIEWED ${reviewed.length} `}
392 </Text>
393 <Text dimColor> {showAll ? '▾ hide' : '▸ show'}</Text>
394 </Button>
395 </Box>
396 )}
397 {showAll && reviewed.map(reviewedRow)}
398 <Box marginTop={1}>
399 <Text dimColor>⋯ </Text>
400 {open.length > 0 && (
401 <Button
402 key="as-all-right"
403 plain
404 onPress={async () => {
405 await update($, list, all => all.map(a => (a.status === 'open' ? { ...a, status: 'right' as const } : a)))
406 await closeIfDone($)
407 }}
408 >
409 <Text dimColor>Mark all right</Text>
410 </Button>
411 )}
412 {open.length > 0 && <Text dimColor> · </Text>}
413 <Button
414 key="as-clear"
415 plain
416 onPress={async () => {
417 await update($, list, () => [])
418 await update($, editing, () => null)
419 }}
420 >
421 <Text dimColor>Clear history</Text>
422 </Button>
423 </Box>
424 </Box>
425 )
426 })
427}
428types/index.d.ts 32 lines1export type Assumption = {
2 id: number
3 /** The assumption, one sentence, as Claude logged it. */
4 text: string
5 /** Why Claude assumed it, if given. */
6 why?: string
7 /** High: a mistake here would be costly to undo. Reviewed first, drawn in yellow. */
8 impact?: 'high' | 'low'
9 status: 'open' | 'right' | 'wrong'
10 /** What the person said it should be, once marked wrong and sent. */
11 correction?: string
12 /** Epoch ms when Claude logged it. */
13 at?: number
14 /** Epoch ms of the last Skip: skipped ones go to the back of the queue. */
15 skippedAt?: number
16}
17
18declare module 'claude-code' {
19 interface PluginState {
20 assumptions: {
21 list: Assumption[]
22 /** Id of the assumption whose correction field is open, or null. */
23 editing: number | null
24 isWorking: boolean
25 /** The next assumption's number; never reset, so numbers never repeat. */
26 nextId: number
27 /** Whether the panel lists the reviewed assumptions too. */
28 showReviewed: boolean
29 }
30 }
31}
32