Grades every request before its turn starts (task class → effort rung, in one low-effort completion) and routes it to the genius reasoning patterns and…

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.tsx 216 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Classified, GeniusDecision, GeniusState } from '../types'
5import {
6 SHAPES_PATH,
7 SHAPE_SKILL_PREFIX,
8 type Shape,
9 classifierPrompt,
10 classifierSystem,
11 isClassifiable,
12 parseClassification,
13 parseShapes,
14 sameRequest,
15 shapeContext,
16 toClassified,
17} from './classify'
18import {
19 GENIUS_DIR,
20 GENIUS_INDEX,
21 GENIUS_PLUGIN,
22 INSTALLED_PLUGINS_PATH,
23 type GeniusRow,
24 geniusContext,
25 installPathOf,
26 parseGeniusIndex,
27} from './genius'
28
29// source: platform.claude.com/docs/en/models/haiku-5-5/whats-new-haiku-5-5: adaptive thinking is on by
30// default and its tokens count toward max_tokens, so a small cap can end after the thinking block;
31// the JSON answer itself is under 60 tokens, unused room costs nothing.
32const CLASSIFIER_MAX_TOKENS = 1024
33const CLASSIFIER_TIMEOUT_MS = 4000 // source: own choice, a prompt must not wait longer on its classifier
34const DECISIONS_CAP = 50 // source: bounds $.state size; a viewer shows the last rows only
35
36const state = atom({ plugin: 'zetetic-genius', key: 'state' } as const, {
37 mode: 'observe',
38 classifierModel: 'haiku',
39 classified: null,
40 classifierError: null,
41 skillShapesLoaded: 0,
42 geniusShapesLoaded: 0,
43 geniusDir: null,
44 decisions: [],
45} as GeniusState)
46
47// The two option lists, read at start; module state a hot reload rebuilds through session.start.
48let skills: Shape[] = []
49let genius: GeniusRow[] = []
50let geniusDir: string | null = null
51
52const expandHome = async ($: EngineInterface, path: string): Promise<string> =>
53 path.startsWith('~/') ? `${(await $.env.get('HOME')) ?? ''}/${path.slice(2)}` : path
54
55const fail = async ($: EngineInterface, what: string, error: unknown): Promise<void> => {
56 await update($, state, (s) => ({ ...s, classifierError: `${what}: ${String(error).slice(0, 100)}` }))
57}
58
59// The skill shapes from the generated routing table; absent, the classifier grades effort alone.
60async function loadSkills($: EngineInterface): Promise<void> {
61 try {
62 skills = parseShapes(await $.fs.read(await expandHome($, SHAPES_PATH)))
63 await update($, state, (s) => ({ ...s, skillShapesLoaded: skills.length }))
64 } catch (error) {
65 skills = []
66 await fail($, SHAPES_PATH, error)
67 }
68}
69
70// The genius shapes from the installed plugin's INDEX.md, found through installed_plugins.json.
71async function loadGenius($: EngineInterface): Promise<void> {
72 try {
73 const installed = await $.fs.read(await expandHome($, INSTALLED_PLUGINS_PATH))
74 const root = installPathOf(installed, GENIUS_PLUGIN)
75 if (root === undefined) throw new Error(`${GENIUS_PLUGIN} is not installed`)
76 geniusDir = `${root}/${GENIUS_DIR}`
77 genius = parseGeniusIndex(await $.fs.read(`${geniusDir}/${GENIUS_INDEX}`))
78 await update($, state, (s) => ({ ...s, geniusDir, geniusShapesLoaded: genius.length }))
79 } catch (error) {
80 genius = []
81 geniusDir = null
82 await fail($, 'genius index', error)
83 }
84}
85
86// One low-effort completion grades the request; anything but a strict answer is no decision.
87async function classify($: EngineInterface, text: string): Promise<Classified | undefined> {
88 const s = await read($, state)
89 const started = await $.clock.now()
90 const r = await $.model.complete({
91 model: s.classifierModel,
92 effort: 'low',
93 system: [{ text: classifierSystem(skills, genius), cache: true }],
94 prompt: classifierPrompt(text),
95 maxTokens: CLASSIFIER_MAX_TOKENS,
96 timeoutMs: CLASSIFIER_TIMEOUT_MS,
97 })
98 const now = await $.clock.now()
99 if (!r.isAnswered) {
100 await fail($, 'classifier', r.reason)
101 return undefined
102 }
103 const c = parseClassification(r.text, skills, genius)
104 if (c === undefined) {
105 await fail($, 'classifier answered off-contract', r.text.slice(0, 80))
106 return undefined
107 }
108 const classified = toClassified(c, text, started, now)
109 await update($, state, (st) => ({ ...st, classified, classifierError: null }))
110 return classified
111}
112
113// The context blocks a graded prompt carries: each genius pattern's procedure, then the skills.
114async function contextFor($: EngineInterface, c: Classified): Promise<string[]> {
115 const blocks: string[] = []
116 for (const pick of c.geniuses) {
117 if (geniusDir === null) break
118 const path = `${geniusDir}/${pick.agent}.md`
119 try {
120 blocks.push(geniusContext(pick.agent, await $.fs.read(path), genius, path))
121 } catch (error) {
122 await fail($, path, error)
123 }
124 }
125 if (c.shapes.length > 0) blocks.push(shapeContext(c.shapes))
126 return blocks
127}
128
129const decide = (s: GeniusState, d: GeniusDecision): GeniusState => ({
130 ...s,
131 decisions: [...s.decisions, d].slice(-DECISIONS_CAP),
132})
133
134export const register: Register = (on, options) => {
135 const mode: GeniusState['mode'] = options.mode === 'enforce' ? 'enforce' : 'observe'
136 const classifierModel = String(options.classifier_model ?? 'haiku')
137 // A hot reload keeps $.state from the previous load: the configured fields are set afresh.
138 const configured = (s: GeniusState): GeniusState => ({ ...s, mode, classifierModel })
139
140 on('session.start', async ($, e, next) => {
141 await update($, state, configured)
142 await loadSkills($)
143 await loadGenius($)
144
145 return next(e)
146 })
147
148 // /clear, /resume and /branch reset $.state and never fire session.start again.
149 on('classic.SessionStart', { source: ['clear', 'resume', 'fork'] }, async ($, e, next) => {
150 await update($, state, configured)
151 await loadSkills($)
152 await loadGenius($)
153
154 return next(e)
155 })
156
157 // Observed, the prompt goes through untouched; enforced, the patterns ride beside it.
158 on('prompt.submit', async ($, e, next) => {
159 if (!isClassifiable(e.text, e.origin)) return next(e)
160 const c = await classify($, e.text)
161 if (c === undefined || (c.geniuses.length === 0 && c.shapes.length === 0)) return next(e)
162 const applied = (await read($, state)).mode === 'enforce'
163 for (const g of c.geniuses)
164 await update($, state, (s) =>
165 decide(s, { at: c.at, kind: 'genius', subject: g.shape, to: `${g.agent} pattern`, applied }),
166 )
167 if (c.shapes.length > 0)
168 await update($, state, (s) =>
169 decide(s, {
170 at: c.at,
171 kind: 'shape',
172 subject: c.shapes.join(' + '),
173 to: c.shapes.map((id) => `${SHAPE_SKILL_PREFIX}${id}`).join(', '),
174 applied,
175 }),
176 )
177 if (!applied) return next(e)
178 const blocks = await contextFor($, c)
179 return blocks.length === 0 ? next(e) : next({ ...e, context: [...(e.context ?? []), ...blocks] })
180 })
181
182 // prompt.submit's turnId is the turn that was running, not the one the prompt starts: the
183 // grade binds to its turn here, by the prompt's opening; a turn of its own carries none.
184 on('turn.start', async ($, e, next) => {
185 const s = await read($, state)
186 const c = s.classified
187 const isOurs = c !== null && c.turnId === null && sameRequest(c.text, e.text)
188 const bound = isOurs && c !== null ? { ...c, turnId: e.turnId } : c?.turnId === null ? c : null
189 await update($, state, (st) => ({ ...st, classified: bound }))
190
191 return next(e)
192 })
193
194 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
195 const s = await read($, state)
196 const c = s.classified
197 if (e.props.hasSurvey || (c === null && s.classifierError === null)) return next(e)
198 const { Box, Text } = $.ui.resolve(e)
199 const picks = [...(c?.geniuses.map((g) => `${g.agent} · ${g.shape}`) ?? []), ...(c?.shapes ?? [])]
200 const graded =
201 c === null
202 ? 'no request graded'
203 : `${c.taskClass} → effort ${c.effort} · ${picks.length === 0 ? 'no pattern' : picks.join(' · ')} · ${c.ms} ms`
204 const trouble = s.classifierError === null ? '' : ` · ${s.classifierError}`
205
206 return (
207 <Box>
208 <Text dimColor>
209 genius {s.mode} · {graded}
210 {trouble}
211 </Text>
212 </Box>
213 )
214 })
215}
216hooks/classify.ts 134 lines1// Pure half of the request classifier: what the cheap model is asked, how its answer is read,
2// and what each answer means for effort. The two option lists come from files read at start.
3
4import type { Classified, Effort, GeniusPick, TaskClass } from '../types'
5import type { GeniusRow } from './genius'
6
7// source: ~/.claude/reference/skill-routing-table.md, generated from the problem-shaped skills'
8// `shapes:` frontmatter (zetetic-team-subagents); read at session start, never copied here.
9export const SHAPES_PATH = '~/.claude/reference/skill-routing-table.md'
10// source: the skills are installed under this plugin name (the Skill tool lists them so).
11export const SHAPE_SKILL_PREFIX = 'zetetic-team-subagents:'
12
13export type Shape = { id: string; description: string }
14
15// `| shape | skill | description |` rows; the header and the separator carry no shape.
16export const parseShapes = (table: string): Shape[] =>
17 table
18 .split('\n')
19 .map((line) => line.split('|').map((cell) => cell.trim()))
20 .filter((cells) => cells.length >= 4 && /^[a-z][a-z-]+$/.test(cells[1] ?? ''))
21 .map((cells) => ({ id: cells[1] ?? '', description: (cells[3] ?? '').replace(/\s+/g, ' ') }))
22
23// source: effort-calibration.md task table: reading and I/O low; a fully specified plan low; a
24// clear bug fix low–medium (medium here, the higher bound); architecture, PRD, research
25// synthesis medium; correctness-critical work high; "genuinely stuck" high is the autopilot's
26// stuck signal, not the classifier's.
27export const TASK_CLASSES: readonly TaskClass[] = ['routine', 'planned', 'bugfix', 'analysis', 'critical']
28const CLASS_EFFORT: Record<TaskClass, Effort> = {
29 routine: 'low',
30 planned: 'low',
31 bugfix: 'medium',
32 analysis: 'medium',
33 critical: 'high',
34}
35export const effortForClass = (cls: TaskClass): Effort => CLASS_EFFORT[cls]
36
37export type Classification = { taskClass: TaskClass; shapes: string[]; geniuses: GeniusPick[] }
38
39// source: own choice, under this length the text continues the previous task ("yes", "go on")
40// and carries no shape of its own.
41export const MIN_CLASSIFIABLE_CHARS = 20
42// source: own choice, the opening of a request names its task; the rest is pasted material.
43export const PROMPT_HEAD_CHARS = 4000
44export const MAX_PICKS = 2 // source: /genius:route recommends 1–3 agents; two patterns at most per prompt
45
46// A slash command runs through command.run; a plugin's own submit is not the person's request.
47export const isClassifiable = (text: string, origin: { kind: string }): boolean =>
48 origin.kind !== 'plugin' && !text.trimStart().startsWith('/') && text.trim().length >= MIN_CLASSIFIABLE_CHARS
49
50export const classifierSystem = (shapes: readonly Shape[], genius: readonly GeniusRow[]): string =>
51 [
52 'You classify one request addressed to a coding agent. Answer with one JSON object and nothing else:',
53 '{"task_class": <class>, "shapes": [<skill id>, ...], "geniuses": [{"agent": <agent>, "shape": <shape>}, ...]}',
54 '',
55 'task_class is exactly one of:',
56 '- routine: reading, listing, searching, formatting, a one-line change, a question answered from files.',
57 '- planned: implementing a plan or spec that is already written out in the request.',
58 '- bugfix: a bug whose cause is clear or quickly located.',
59 '- analysis: an architecture decision, a design, a research synthesis, a multi-file feature.',
60 '- critical: formal correctness, concurrency, security, billing, data loss; a wrong answer is worse than a slow one.',
61 '',
62 `shapes and geniuses together hold at most ${MAX_PICKS} picks whose trigger the request clearly matches; both [] when none does.`,
63 // source: agents/genius/INDEX.md, its rule, verbatim.
64 'Rule: if no shape below matches the problem, do not force a genius agent.',
65 'A pick fits only when the request has that structure, never because the topic is adjacent.',
66 'Prefer a genius shape (a precise reasoning procedure) over a skill when both would fit; never pick both for the same structure.',
67 '',
68 'Skills (problem-shaped methods):',
69 ...shapes.map((s) => `- ${s.id}: ${s.description}`),
70 '',
71 'Genius shapes (agent in parentheses, then the trigger):',
72 ...genius.map((g) => `- ${g.shape} (${g.agent}): ${g.trigger}`),
73 ].join('\n')
74
75export const classifierPrompt = (text: string): string => text.slice(0, PROMPT_HEAD_CHARS)
76
77const asPick = (v: unknown, known: readonly GeniusRow[]): GeniusPick | undefined => {
78 const o = v as { agent?: unknown; shape?: unknown } | null
79 if (o === null || typeof o !== 'object' || typeof o.agent !== 'string' || typeof o.shape !== 'string') return undefined
80 return known.some((g) => g.agent === o.agent && g.shape === o.shape) ? { agent: o.agent, shape: o.shape } : undefined
81}
82
83// Strict: a JSON object, fenced or bare; an unknown class, skill or genius pair voids the answer.
84export const parseClassification = (
85 answer: string,
86 shapes: readonly Shape[],
87 genius: readonly GeniusRow[],
88): Classification | undefined => {
89 const match = /\{[\s\S]*\}/.exec(answer)
90 if (match === null) return undefined
91 let raw: unknown
92 try {
93 raw = JSON.parse(match[0])
94 } catch {
95 return undefined
96 }
97 const obj = raw as { task_class?: unknown; shapes?: unknown; geniuses?: unknown }
98 const cls = obj.task_class
99 if (typeof cls !== 'string' || !(TASK_CLASSES as readonly string[]).includes(cls)) return undefined
100 const knownSkills = new Set(shapes.map((s) => s.id))
101 const skills = Array.isArray(obj.shapes) ? obj.shapes : []
102 if (!skills.every((s) => typeof s === 'string' && knownSkills.has(s))) return undefined
103 const picks = Array.isArray(obj.geniuses) ? obj.geniuses.map((g) => asPick(g, genius)) : []
104 if (picks.some((p) => p === undefined)) return undefined
105 const geniuses = (picks as GeniusPick[]).filter((p, i, all) => all.findIndex((q) => q.agent === p.agent) === i)
106 const uniqueSkills = [...new Set(skills as string[])]
107 const room = Math.max(0, MAX_PICKS - geniuses.slice(0, MAX_PICKS).length)
108 return { taskClass: cls as TaskClass, shapes: uniqueSkills.slice(0, room), geniuses: geniuses.slice(0, MAX_PICKS) }
109}
110
111export const toClassified = (c: Classification, text: string, startedAt: number, now: number): Classified => ({
112 turnId: null,
113 text,
114 taskClass: c.taskClass,
115 effort: effortForClass(c.taskClass),
116 shapes: c.shapes,
117 geniuses: c.geniuses,
118 ms: now - startedAt,
119 at: now,
120})
121
122// What the model reads beside the prompt when a skill fits: load it first.
123export const shapeContext = (ids: readonly string[]): string =>
124 [
125 'zetetic-genius: this request matches a reasoning shape. Before answering, invoke the Skill tool for',
126 ...ids.map((id) => ` ${SHAPE_SKILL_PREFIX}${id}`),
127 'and follow its method. Say in one line which shape you applied.',
128 ].join('\n')
129
130// source: own choice, the opening of a prompt identifies it at turn.start.
131export const BIND_HEAD_CHARS = 200
132export const sameRequest = (a: string, b: string): boolean =>
133 a.slice(0, BIND_HEAD_CHARS) === b.slice(0, BIND_HEAD_CHARS)
134hooks/genius.ts 77 lines1// Pure side of the genius patterns: where they are installed, how INDEX.md routes by shape, and
2// what part of a pattern file rides beside the prompt.
3
4// source: ~/.claude/plugins/installed_plugins.json, the engine's record of installed plugins;
5// `plugins["<name>@<marketplace>"][0].installPath` is the folder a session runs.
6export const INSTALLED_PLUGINS_PATH = '~/.claude/plugins/installed_plugins.json'
7export const GENIUS_PLUGIN = 'zetetic-team-subagents@zetetic-marketplace'
8// source: the plugin's layout, agents/genius/<name>.md and agents/genius/INDEX.md.
9export const GENIUS_DIR = 'agents/genius'
10export const GENIUS_INDEX = 'INDEX.md'
11
12export const installPathOf = (installedPluginsJson: string, plugin: string): string | undefined => {
13 let parsed: { plugins?: Record<string, { installPath?: unknown }[]> }
14 try {
15 parsed = JSON.parse(installedPluginsJson) as typeof parsed
16 } catch {
17 return undefined
18 }
19 const path = parsed.plugins?.[plugin]?.[0]?.installPath
20 return typeof path === 'string' && path !== '' ? path : undefined
21}
22
23// One INDEX.md row: `| **shape** | trigger | [agent](agent.md) | key move |`.
24export type GeniusRow = { shape: string; trigger: string; agent: string; keyMove: string }
25
26const ROW = /^\|\s*\*\*([a-z0-9-]+)\*\*\s*\|\s*(.*?)\s*\|\s*\[([a-z0-9-]+)\]\([^)]*\)\s*\|\s*(.*?)\s*\|\s*$/
27
28export const parseGeniusIndex = (md: string): GeniusRow[] =>
29 md
30 .split('\n')
31 .map((line) => ROW.exec(line))
32 .filter((m): m is RegExpExecArray => m !== null)
33 .map((m) => ({ shape: m[1] ?? '', trigger: m[2] ?? '', agent: m[3] ?? '', keyMove: m[4] ?? '' }))
34
35// The sections of a pattern file are XML-like tags; the procedure the model needs is the
36// identity's opening, the workflow and the output format (about 4–5 K characters), not the
37// canonical moves (11 K median), which stay one Read away.
38export const sectionOf = (file: string, tag: string): string | undefined => {
39 const m = new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`).exec(file)
40 return m === null ? undefined : (m[1] ?? '').trim()
41}
42
43export const identityOpening = (file: string): string => {
44 const identity = sectionOf(file, 'identity') ?? ''
45 return identity.split(/\n\s*\n/).find((p) => p.trim() !== '') ?? ''
46}
47
48// The pattern's own effort, from its frontmatter; the owner decides whether it floors a turn.
49export const patternEffort = (file: string): string | undefined => {
50 const m = /^effort:\s*([a-z]+)\s*$/m.exec(file.split(/^---\s*$/m)[1] ?? '')
51 return m === null ? undefined : m[1]
52}
53
54export const geniusContext = (
55 agent: string,
56 file: string,
57 rows: readonly GeniusRow[],
58 filePath: string,
59): string => {
60 const matched = rows.filter((r) => r.agent === agent)
61 return [
62 `zetetic-genius: apply the ${agent} reasoning pattern to this request, before anything else.`,
63 '',
64 identityOpening(file),
65 '',
66 ...matched.map((r) => `Shape ${r.shape}: ${r.trigger}. Key move: ${r.keyMove}`),
67 '',
68 'Workflow:',
69 sectionOf(file, 'workflow') ?? '(no workflow section)',
70 '',
71 'Output format:',
72 sectionOf(file, 'output-format') ?? '(no output-format section)',
73 '',
74 `The full pattern, with its canonical moves and blind spots, is ${filePath}; read it when a step needs more than the workflow says. Say in one line which pattern you applied.`,
75 ].join('\n')
76}
77types/index.d.ts 48 lines1// The request classifier's contract: what it decided about the current prompt.
2
3// source: ~/.claude/reference/agent-reference/effort-calibration.md § Effort levels (task table).
4export type TaskClass = 'routine' | 'planned' | 'bugfix' | 'analysis' | 'critical'
5export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max'
6
7// One genius reasoning pattern picked for the request, by the shape that matched.
8export type GeniusPick = { agent: string; shape: string }
9
10// What the classifier said about one prompt: pending (turnId null) until turn.start binds it.
11export type Classified = {
12 turnId: string | null
13 text: string
14 taskClass: TaskClass
15 effort: Effort
16 shapes: string[]
17 geniuses: GeniusPick[]
18 ms: number
19 at: number
20}
21
22export type GeniusDecision = {
23 at: number
24 kind: 'shape' | 'genius'
25 subject: string
26 to: string
27 applied: boolean
28}
29
30export type GeniusState = {
31 mode: 'observe' | 'enforce'
32 classifierModel: string
33 classified: Classified | null
34 classifierError: string | null
35 skillShapesLoaded: number
36 geniusShapesLoaded: number
37 geniusDir: string | null
38 decisions: GeniusDecision[]
39}
40
41declare module 'claude-code' {
42 interface PluginState {
43 'zetetic-genius': {
44 state: GeniusState
45 }
46 }
47}
48