/wiki <wiki/**.md>: compile a Cortex wiki page (Markdown with ```tikz blocks) to PDF through pandoc and xelatex, and open it.

Claude Code mods for the ai-architect.tools harness, one concern per mod, state shared through dependencies:
| Mod | Owns | Depends on |
|---|---|---|
cortex-guard | refusals: wiki pages only through wiki_write, worktrees inside <repo>/.claude/worktrees/ | nothing |
cortex-wiki | /wiki <wiki/**.md> to PDF | nothing |
zetetic-genius | state: the request grade (task class → effort), the genius patterns and skills it matches | nothing |
zetetic-autopilot | context, policy: effort ladder, model routing, pressure, lean results | zetetic-genius |
cortex-cockpit | stats, ledger, tally, stages, hygiene; the /cortex pane draws the rest | the three above |
harness-fleet | fleet: the owner's plugins, installed vs offered version, open PRs with CI, open issues (a defect is taken and fixed, a feature request goes to the owner); /fleet. Reads only (gh pr list, gh issue list, git remote): every outward action is a button the owner presses, which puts a prompt in front of the model | guard, genius, autopilot |
Each mod is validated, tested and type-checked on its own:
cd mods/<mod> && claude plugin validate . && claude plugin test && npx -y -p typescript tsc -p .
tsc needs the engine to have loaded the mod once (it lays .claude-plugin/types/). For hot reload in a session, link the mod into that session's ~/.claude/dev-mods/<session>/ folder; the engine watches the folder a link names.
.claude-plugin/marketplace.json lists each mod as "source": "./mods/<mod>", so the repository is the marketplace. From another machine:
/plugin install <mod> --marketplace cdeust/claude-mods
y adds the marketplace, then the user scope. On this machine a folder marketplace reads the mods from the checkout, with no copy: claude plugin marketplace add <this folder>, then /plugin install <mod> and /reload-plugins after an edit.
hooks/register.ts 31 lines1import type { Register } from 'claude-code'
2
3import { pandocArgv, toPandocSource, wikiTarget } from './wiki'
4
5const TEX_TIMEOUT_MS = 120_000 // source: own choice, a 47 KB page with TikZ built in under a minute
6
7export const register: Register = (on) => {
8 on('session.start', async ($, e, next) => {
9 await $.command.register({
10 name: 'wiki',
11 description: 'Compile a wiki page (Markdown with ```tikz blocks) to PDF and open it',
12 argumentHint: '<wiki/**.md>',
13 })
14
15 return next(e)
16 })
17
18 on('command.run', { command: 'wiki' }, async ($, e) => {
19 const target = wikiTarget(e.args)
20 if (!target.ok) return { text: target.reason }
21 const tmp = ((await $.env.get('TMPDIR')) ?? '/tmp').replace(/\/$/, '')
22 const out = `${tmp}/cortex-wiki-${target.path.replace(/[^A-Za-z0-9]+/g, '-')}.pdf`
23 const source = toPandocSource(await $.fs.read(target.path))
24 const built = await $.process.run(pandocArgv('-', out), { stdin: source, timeoutMs: TEX_TIMEOUT_MS })
25 if (built.exitCode !== 0) return { text: `TeX build failed:\n${built.stderr.slice(0, 600)}` }
26 await $.process.run(['open', out])
27
28 return { text: `Compiled and opened ${out}` }
29 })
30}
31hooks/wiki.ts 47 lines1// Pure side of the wiki renderer: Markdown with ```tikz blocks becomes a pandoc job.
2
3// source: pandoc raw attribute syntax, a ```{=latex} fence passes its body to TeX untouched
4// (pandoc 3.9 manual, "Extension: raw_attribute"); proven by compiling a tikzpicture here.
5const FENCE = /^```tikz[ \t]*\n([\s\S]*?)^```[ \t]*$/gm
6
7// source: forest.sty is absent from this machine's TeX (kpsewhich forest.sty, 2026-10-07), so
8// trees use TikZ's own `trees` library, part of pgf.
9const PREAMBLE = [
10 '\\usepackage{tikz}',
11 '\\usetikzlibrary{trees,arrows.meta,positioning,shapes.geometric}',
12]
13
14export const toPandocSource = (markdown: string): string =>
15 markdown.replace(
16 FENCE,
17 (_all, body: string) =>
18 `\`\`\`{=latex}\n\\begin{tikzpicture}\n${body}\\end{tikzpicture}\n\`\`\``,
19 )
20
21export const diagramCount = (markdown: string): number => (markdown.match(FENCE) ?? []).length
22
23export const pandocArgv = (src: string, pdf: string): string[] => [
24 'pandoc',
25 '-s',
26 '-f',
27 'markdown',
28 '-t',
29 'latex',
30 '--pdf-engine=xelatex',
31 ...PREAMBLE.flatMap((line) => ['-V', `header-includes=${line}`]),
32 src,
33 '-o',
34 pdf,
35]
36
37export type Target = { ok: true; path: string } | { ok: false; reason: string }
38
39// Only wiki pages: a Markdown file under a wiki/ directory, no parent escapes.
40export const wikiTarget = (arg: string): Target => {
41 const path = arg.trim()
42 if (path === '') return { ok: false, reason: 'usage: /wiki <path to a wiki/**.md page>' }
43 if (path.split('/').includes('..')) return { ok: false, reason: 'no ".." in the path' }
44 if (!/(^|\/)wiki\/.+\.md$/.test(path)) return { ok: false, reason: 'not a wiki/**.md page' }
45 return { ok: true, path }
46}
47