Resources for MATS fellows

Resources for MATS (ML Alignment Theory Scholars) fellows using Claude Code.
Literature review (/mats:lit-review) — Staged literature review pipeline. Searches arXiv, Semantic Scholar, and Google Scholar; collects LessWrong and Alignment Forum posts. Downloads papers, converts to markdown, generates summaries with relevance scoring, and produces a final report. Resumable if interrupted. Works for any research topic, not just AI safety.
hooks/register.tsx 60 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { noticeRows } from './notice-band'
5import type { Notice, NoticeAction } from './notice-band'
6
7const PLUGIN = 'mats'
8const notices = atom({ plugin: 'mats', key: 'notices' } as const, [] as Notice[])
9const dismissed = atom({ plugin: 'mats', key: 'dismissed' } as const, [] as string[])
10
11/** Runs the plugin's session-start check and keeps its rows; none when it fails. */
12async function check($: EngineInterface, projectDir: string): Promise<void> {
13 try {
14 // The hive plugin draws the same install row, so mats leaves it to hive. No API lists
15 // plugins; the hive plugin's commands are its mark.
16 if ((await $.command.list()).some(command => command.name.startsWith('hive:'))) return
17 const { exitCode, stdout } = await $.process.run(['bash', `${$.plugin.root}/scripts/check.sh`], {
18 env: { CLAUDE_PLUGIN_ROOT: $.plugin.root, CLAUDE_PROJECT_DIR: projectDir },
19 })
20 const found = exitCode === 0 && stdout.trim() ? (JSON.parse(stdout) as { notices: Notice[] }).notices : []
21 await update($, notices, () => found)
22 } catch {
23 // A check that fails shows nothing rather than a wrong row.
24 }
25}
26
27export const register: Register = on => {
28 // The SessionStart hook's lifecycle: startup and /clear, awaited.
29 on('classic.SessionStart', async ($, e, next) => {
30 if (e.source === 'startup' || e.source === 'clear') await check($, e.cwd)
31 return next(e)
32 })
33
34 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
35 const [below, found, hidden] = await Promise.all([next(e), read($, notices), read($, dismissed)])
36 if (e.props.hasSurvey) return below
37
38 const rows = (found as Notice[]).filter(notice => !hidden.includes(notice.id))
39 if (rows.length === 0) return below
40
41 const onAction = async (_notice: Notice, action: NoticeAction) => {
42 // The install script asks questions, so it runs in a terminal of the person's own.
43 if (action.kind !== 'copy' || !action.command) return
44 const copied = await $.ui.copy({ text: action.command, surface: e.surface })
45 $.ui.toast(copied.isCopied ? 'Copied: paste it into a terminal' : `Run in a terminal: ${action.command}`)
46 }
47 const onDismiss = (notice: Notice) => update($, dismissed, list => [...list, notice.id])
48
49 const ui = $.ui.resolve(e)
50 const drawn = noticeRows(ui, PLUGIN, rows, { onAction, onDismiss })
51
52 return (
53 <ui.Box flexDirection="column">
54 {drawn}
55 {below}
56 </ui.Box>
57 )
58 })
59}
60hooks/notice-band.tsx 92 lines1import type { EngineInterface, RenderElement } from 'claude-code'
2
3// One plugin's rows in the band above the prompt, drawn without `$`: each
4// plugin's own `ui.render` hook reads its state and stacks this over what the
5// plugins beneath drew, so third-party plugins on the band keep working.
6//
7// A plugin cannot import another's files, so each plugin that shows notices
8// carries this file; plugins/notice-band.test.ts keeps the copies identical.
9// The `notices` its check prints are this file's `Notice` as JSON.
10
11export type Severity = 'urgent' | 'problem' | 'action' | 'info'
12
13export type NoticeAction = {
14 id: string
15 label: string
16 // command: a slash command; copy: `command` goes on the clipboard, for a
17 // terminal command that is interactive; plugin: the plugin's own handler decides.
18 kind: 'command' | 'copy' | 'plugin'
19 command?: string
20}
21
22export type Notice = {
23 id: string
24 severity: Severity
25 // The whole message: rows wrap rather than hide what the person needs to act.
26 text: string
27 actions?: NoticeAction[]
28 // Set on "updated" notices: a dismissal holds until the plugin's next
29 // version. Without it, a dismissal holds for this session only.
30 version?: string
31 // What a pressed button is doing ("restarting…"): drawn after the text in
32 // place of the buttons, so the row never looks inert. Set by the plugin.
33 busy?: string
34}
35
36export type NoticeHandlers = {
37 onAction: (notice: Notice, action: NoticeAction) => unknown
38 onDismiss: (notice: Notice) => unknown
39}
40
41const MARK: Record<Severity, { rank: number; glyph: string; color: string }> = {
42 urgent: { rank: 0, glyph: '✖', color: 'error' },
43 problem: { rank: 1, glyph: '!', color: 'warning' },
44 action: { rank: 2, glyph: '›', color: 'suggestion' },
45 info: { rank: 3, glyph: '↑', color: 'ide' },
46}
47
48export function noticeRows(
49 ui: ReturnType<EngineInterface['ui']['resolve']>,
50 plugin: string,
51 notices: Notice[],
52 { onAction, onDismiss }: NoticeHandlers,
53): RenderElement[] {
54 const { Box, Button, Text } = ui
55
56 return [...notices]
57 .sort((a, b) => MARK[a.severity].rank - MARK[b.severity].rank)
58 .map(notice => {
59 const mark = MARK[notice.severity]
60 const key = `${plugin}:${notice.id}`
61
62 const buttons = (notice.busy ? [] : (notice.actions ?? [])).map((action, index) => (
63 <Button
64 key={`${key}:${action.id}`}
65 label={action.label}
66 variant={index === 0 ? 'primary' : 'secondary'}
67 onPress={() => onAction(notice, action)}
68 />
69 ))
70 // An info row is a state, not a request: the band's own collapse hides it.
71 if (notice.severity !== 'info' && !notice.busy) {
72 buttons.push(<Button key={`${key}:dismiss`} label="Dismiss" dimColor onPress={() => onDismiss(notice)} />)
73 }
74
75 // The buttons wrap under the text where the row does not fit.
76 return (
77 <Box key={key} flexDirection="row" flexWrap="wrap" justifyContent="space-between" columnGap={2}>
78 <Text>
79 <Text color={mark.color} bold>
80 {mark.glyph}{' '}
81 </Text>
82 <Text bold>{plugin}</Text> {notice.busy ? `${notice.text} · ${notice.busy}` : notice.text}
83 </Text>
84 {/* Grows to fill its line, so the buttons stay right-aligned when they wrap. */}
85 <Box flexDirection="row" flexGrow={1} justifyContent="flex-end" columnGap={1}>
86 {buttons}
87 </Box>
88 </Box>
89 )
90 })
91}
92types/index.d.ts 7 lines1declare module 'claude-code' {
2 interface PluginState {
3 // `notices`: the check's rows, hooks/notice-band.tsx's Notice as parsed JSON.
4 mats: { notices: unknown[]; dismissed: string[] }
5 }
6}
7