Adam's personal skills, agents, and commands

Adam's personal Claude Code skills, agents, commands, and memory — packaged as an installable plugin marketplace and auto-synced to git.
.claude-plugin/marketplace.json # marketplace catalog
plugins/skillz/ # the plugin (installable via /plugin install)
.claude-plugin/plugin.json
skills/ # SKILL.md per skill
agents/
commands/
memory/ # synced personal memory (not part of plugin)
scripts/ # validate + auto-push helpers
mise.toml # tool versions + tasks
hk.pkl # lint pipeline
From any Claude Code session:
/plugin marketplace add askclara/skillz # once GitHub remote is set
/plugin install skillz@askclara-skillz
For local dev:
/plugin marketplace add /Users/adam/src/skillz
/plugin install skillz@askclara-skillz
mise install # install hk, lychee, prettier, typos, etc.
mise run lint # run all linters
mise run fix # auto-fix what can be fixed
mise run validate-skills # check SKILL.md frontmatter + manifest JSON
hk install # set up the pre-commit hook (one-time)
mise run watch-and-push watches plugins/, memory/, and .claude-plugin/ and commits + pushes on any change (5 s debounce).
To run it as a launchd background service on macOS, drop this into ~/Library/LaunchAgents/com.askclara.skillz-autopush.plist and launchctl load it:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.askclara.skillz-autopush</string>
<key>WorkingDirectory</key><string>/Users/adam/src/skillz</string>
<key>ProgramArguments</key>
<array>
<string>/Users/adam/.local/bin/mise</string>
<string>run</string>
<string>watch-and-push</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/tmp/skillz-autopush.log</string>
<key>StandardErrorPath</key><string>/tmp/skillz-autopush.log</string>
</dict></plist>
| Linter | Purpose |
|---|---|
markdown_lint | Markdown structure (headings, lists, code blocks) |
prettier | Format markdown tables + JSON/YAML |
lychee | Validate every URL + relative link + anchor in markdown |
typos | Catch typos in skill names + descriptions |
detect_private_key | Block accidentally-committed private keys |
check_added_large_files | Block oversized files (defaults to 500 KB) |
actionlint | Lint our own GitHub Actions workflow |
validate-skills | SKILL.md frontmatter + plugin.json / marketplace.json shape |
The same hk check runs locally (pre-commit) and in CI (.github/workflows/lint.yml).
memory/ is intended as the canonical store for the auto-memory system. To use it as the project memory dir for this repo, symlink it:
ln -sfn /Users/adam/src/skillz/memory \
/Users/adam/.claude/projects/-Users-adam-src-skillz/memoryhooks/register.tsx 224 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Ask } from '../types'
5
6const PANE = 'vf'
7const STORE_KEY = 'vfOff'
8const off = atom({ plugin: 'skillz', key: 'vfOff' } as const, [])
9
10export const DECISIONS = [
11 { id: 'emoji', label: 'Emoji anchors', rule: 'emoji anchors on concepts or status lines' },
12 { id: 'boxes', label: 'L1/L2/L3 boxed headers', rule: 'box-drawing headers (╔ ┏ ┌ or borderless ══ ━━ ──)' },
13 { id: 'dashboard', label: 'Status-bucket dashboards', rule: 'goal/bucket dashboards with dot leaders' },
14 { id: 'question', label: 'Rounded question block', rule: 'the rounded ╭ │ ╰ question block; ask inline instead' },
15 { id: 'summary', label: 'Completion summary box', rule: 'a boxed completion summary; one plain line instead' },
16 { id: 'merged', label: '"PR merged" block', rule: 'the 🚢 "PR merged" block' },
17 { id: 'done', label: '"We done here" banner', rule: 'the ▛▀ "WE DONE HERE" banner' },
18 { id: 'copy', label: 'Copy-to-clipboard links', rule: 'pbcopy:// copy links after snippets' },
19] as const
20
21export const overrides = (ids: readonly string[]) => {
22 const dropped = DECISIONS.filter(d => ids.includes(d.id))
23 if (dropped.length === 0) return undefined
24 return [
25 'The user switched off these visual-formatting decisions (this overrides the visual-formatting and copy-link rules):',
26 ...dropped.map(d => `- do not use ${d.rule}`),
27 ].join('\n')
28}
29
30const MARKETPLACE = 'northisup-skillz'
31const PLUGINS = ['skillz', 'tmux-session-resume', 'guarded-worktrees'] as const
32
33const lastLine = (out: string) => out.trim().split('\n').at(-1) ?? ''
34
35const VF_TOOL = 'mcp__skillz__vf_set'
36const ASK_TOOL = 'mcp__skillz__ask'
37const pending = atom({ plugin: 'skillz', key: 'ask' } as const, null)
38
39export const answers = (ask: Ask, text: string) =>
40 ask.options.some(o => new RegExp(`(^|[^A-Za-z0-9.])${o.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^A-Za-z0-9.]|$)`).test(text))
41
42export const askBlock = (ask: Ask) =>
43 [
44 '╭────────────────────────────────────────',
45 `│ ❓ ${ask.question}`,
46 '│',
47 ...ask.options.map(o => `│ ${o.id}${o.recommended ? ' ⭐' : ''} ${o.label}${o.detail ? ` ..... ${o.detail}` : ''}`),
48 '╰────────────────────────────────────────',
49 `say ${ask.options.map(o => `**${o.id}**`).join(' or ')}`,
50 ].join('\n')
51
52const setEnabled = async ($: EngineInterface, id: string, enabled: boolean) => {
53 const next = await update($, off, list => (enabled ? list.filter(x => x !== id) : list.includes(id) ? list : [...list, id]))
54 await $.store.set(STORE_KEY, next)
55 return next
56}
57
58export const checklist = (ids: readonly string[]) =>
59 DECISIONS.map((d, i) => `${i + 1}. ${ids.includes(d.id) ? '[ ]' : '[x]'} ${d.label}`).join('\n')
60
61export const register: Register = on => {
62 on('session.start', async ($, e, next) => {
63 const saved = await $.store.get(STORE_KEY)
64 if (Array.isArray(saved)) await update($, off, () => saved.filter(x => typeof x === 'string'))
65 await $.command.register({ name: 'skillz-update', description: 'Update skillz plugins, then reload plugins and skills' })
66 await $.tool.register({
67 name: 'vf_set',
68 isDeferred: false,
69 description:
70 'Switch a visual-formatting decision on or off for this user, saved across sessions. Call it when the user says they want more or less of one (e.g. "stop with the boxes", "bring back the done banner"), then follow the new setting. Returns the full checklist.',
71 inputSchema: {
72 type: 'object',
73 properties: {
74 id: { type: 'string', enum: DECISIONS.map(d => d.id) },
75 enabled: { type: 'boolean' },
76 },
77 required: ['id', 'enabled'],
78 },
79 })
80 await $.tool.register({
81 name: 'ask',
82 isDeferred: false,
83 description:
84 'Pin a question with labeled options above the user\'s prompt until they answer; picking one sends its id as their reply. Call it whenever a reply ends asking the user to pick (the question block of the visual-formatting rule), with the same ids. Returns the block as text: also print it as the last thing in your reply, then end your turn.',
85 inputSchema: {
86 type: 'object',
87 properties: {
88 question: { type: 'string' },
89 options: {
90 type: 'array',
91 minItems: 2,
92 maxItems: 9,
93 items: {
94 type: 'object',
95 properties: {
96 id: { type: 'string', description: 'globally unique option id, e.g. G1' },
97 label: { type: 'string', description: 'who acts (Agent/User) and what happens' },
98 detail: { type: 'string', description: 'cost or tradeoff' },
99 recommended: { type: 'boolean' },
100 },
101 required: ['id', 'label'],
102 },
103 },
104 },
105 required: ['question', 'options'],
106 },
107 })
108 await $.command.register({ name: 'vf', description: 'Toggle visual-formatting decisions' })
109 return next(e)
110 })
111
112 on('command.run', { command: 'vf' }, async $ => {
113 await $.ui.open({ id: PANE, title: 'Visual formatting', focus: true, closeOnEscape: true })
114 // shortcut: chat text is read-only, toggling happens in the pane; drop once the pane is trusted
115 return { text: `Visual formatting (toggle in the pane):\n${checklist(await read($, off))}` }
116 })
117
118 on('tool.call', { tool: VF_TOOL }, async ($, e) => {
119 if (!DECISIONS.some(d => d.id === e.id) || typeof e.enabled !== 'boolean')
120 return { deny: `unknown decision ${String(e.id)} or non-boolean enabled` }
121 return { result: checklist(await setEnabled($, e.id, e.enabled)) }
122 })
123
124 on('tool.call', { tool: ASK_TOOL }, async ($, e) => {
125 const options = Array.isArray(e.options) ? e.options.filter(o => typeof o?.id === 'string' && typeof o?.label === 'string') : []
126 if (typeof e.question !== 'string' || options.length < 2) return { deny: 'need a question and at least two options with id and label' }
127 const ask: Ask = { question: e.question, options }
128 await update($, pending, () => ask)
129 return { result: askBlock(ask) }
130 })
131
132 // Sticky: only a message naming one of its option ids answers it.
133 on('prompt.submit', async ($, e, next) => {
134 const ask = await read($, pending)
135 if (ask && answers(ask, e.text)) await update($, pending, () => null)
136 return next(e)
137 })
138
139 // commands/update.md stays as the manual fallback for when this module isn't loaded.
140 on('command.run', { command: 'skillz-update' }, async $ => {
141 const minutes = { timeoutMs: 180_000 }
142 const market = await $.process.run(['claude', 'plugin', 'marketplace', 'update', MARKETPLACE], minutes)
143 if (market.exitCode !== 0) return { text: `Marketplace update failed:\n${lastLine(market.stderr || market.stdout)}` }
144
145 const lines: string[] = []
146 for (const p of PLUGINS) {
147 const r = await $.process.run(['claude', 'plugin', 'update', `${p}@${MARKETPLACE}`], minutes)
148 lines.push(`${p}: ${lastLine(r.stdout || r.stderr)}`)
149 }
150 // $.command.run is refused from inside this hook, so the reloads start once it has answered.
151 void (async () => {
152 await $.clock.sleep(0)
153 await $.command.run({ command: 'reload-skills', args: '' })
154 // Last: reloading plugins unloads this module, ending this chain.
155 await $.command.run({ command: 'reload-plugins', args: '' })
156 })().catch((err: unknown) => $.ui.toast(`skillz: reload failed, run /reload-plugins and /reload-skills (${String(err)})`))
157
158 return { text: [...lines, 'Reloading plugins and skills…'].join('\n') }
159 })
160
161 on('prompt.compose', async ($, e, next) => {
162 const result = await next(e)
163 const text = overrides(await read($, off))
164 if (!text) return result
165 return { sections: [...result.sections, { id: 'skillz:vf-overrides', text, scope: 'session' }] }
166 })
167
168 // Composes with whatever else draws the band (cc-statusline) instead of replacing it.
169 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
170 const below = await next(e)
171 const ask = await read($, pending)
172 if (e.props.hasSurvey || !ask) return below
173 const { Box, Text, Button } = $.ui.resolve(e)
174 // ctrl+x tab lands on the question, not on whatever else is in the band.
175 const focused = ask.options.find(o => o.recommended) ?? ask.options[0]
176
177 return (
178 <Box flexDirection="column">
179 {below}
180 <Box flexDirection="column" borderStyle="round" paddingX={1}>
181 <Box>
182 <Text bold>❓ {ask.question} </Text>
183 <Button key="ask-dismiss" plain role="dismiss" onPress={() => update($, pending, () => null)}>
184 ✕
185 </Button>
186 </Box>
187 {ask.options.map(o => (
188 <Button
189 key={`ask-${o.id}`}
190 plain
191 {...(o === focused && { autoFocus: true as const })}
192 onPress={async () => {
193 await update($, pending, () => null)
194 await $.prompt.submit({ text: o.id, asUser: true })
195 }}>
196 {o.id}
197 {o.recommended ? ' ⭐' : ''} {o.label}
198 {o.detail ? <Text dimColor> ..... {o.detail}</Text> : ''}
199 </Button>
200 ))}
201 <Text dimColor>ctrl+x tab, arrows, Enter</Text>
202 </Box>
203 </Box>
204 )
205 })
206
207 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
208 const { Box, Text, Button } = $.ui.resolve(e)
209 const ids = await read($, off)
210 const toggle = (id: string) => () => setEnabled($, id, ids.includes(id))
211
212 return (
213 <Box flexDirection="column">
214 <Text dimColor>Unchecked = Claude stops doing it. Saved across sessions.</Text>
215 {DECISIONS.map((d, i) => (
216 <Button key={d.id} plain hotkey={String(i + 1)} onPress={toggle(d.id)}>
217 {ids.includes(d.id) ? '[ ]' : '[x]'} {d.label}
218 </Button>
219 ))}
220 </Box>
221 )
222 })
223}
224types/index.d.ts 14 lines1export type DecisionId = string
2export type AskOption = { id: string; label: string; detail?: string; recommended?: boolean }
3export type Ask = { question: string; options: AskOption[] }
4
5declare module 'claude-code' {
6 interface McpToolInputs {
7 'mcp__skillz__vf_set': { id: DecisionId; enabled: boolean }
8 'mcp__skillz__ask': { question: string; options: AskOption[] }
9 }
10 interface PluginState {
11 'skillz': { vfOff: DecisionId[]; ask: Ask | null }
12 }
13}
14