Claude Code adapter for hq: your persistent specialist agents, with their memory and projects, as subagents in every session.

Want a personal agent like Grok bots, Muse or Dots, but on your own computer, working with your files?<br> Want each agent kept separate, with its own skills, tools, memory and accounts?<br> Want one place to manage them all, with a history you can roll back?<br> Rather start from an agent someone already built than from scratch?
That's what hq is built for.
Persistent specialist agents you can call from any repo. Each has its own skills and layered memory, and each is its own git repo: install one from a GitHub link, change it, roll it back, share it or keep it private. Persistent means durable identity and memory, not a long-running process: every call is a fresh session that picks up where the last one left off.
How it fits together and where it's going: docs/architecture.md; one run, drawn: docs/how-hq-runs.html; every file and which one to open for a change: docs/codebase-map.md.
<img alt="How an hq agent runs: entry points, hq-pkg resolve, snapshot, bin/hq, Claude Code run; hq-auth with shared services and auth.json; MCP server" src="docs/img/how-hq-runs-light.png">
git clone https://github.com/Ying-Kai-Liao/hq ~/hq && ~/hq/install.sh # ~/hq: any folder you like
hq builder # meet the starter: it learns who you are, then builds your agents with you
hq install <owner>/<repo> # an agent someone shared: any GitHub repo with an agent.md at its root
hq new reading "tracks what I read" # or start one yourself
hq doctor # check everything, with what to fix
hq backup # everything to private GitHub repos: state, memories, your agents
This repo is the kernel only: no agents ship with it. install.sh links the hq CLI, installs the Claude Code adapter plugin (adapters/claude-code, served by this repo's own marketplace), then runs hq init: it checks what hq needs (git and its identity, Claude Code, gh), makes ~/.hq and the agents folder (each a git repo), a user.md for your agents to fill in, asks once whether to back everything up to private GitHub repos (off by default: it stays on this Mac, which Time Machine backs up; hq backup later), and offers the builder, the agent that builds the others, then lists the agents people shared (GitHub repos tagged hq-agent) to install any by number; hq market shows that list any time. Run hq init again any time; it only does what is missing. On a new Mac, install.sh finds your backup through your GitHub login and offers to bring it back. Then, in any Claude Code session: "have the designer critique this page", /hq branding <task>, or the mcp__hq__invoke tool. Agents appear as hq:<name>; your main session only sees one line per specialist, and their skills load only when they're called. Or run one directly in a terminal: hq branding.
The plugin reads the installed agents at session start: edit an agent.md or PROTOCOL.md and the next session has it. On every spawn it resolves the project, puts it and the agent's long-term memory in front of the task, and when the agent stops without writing its session file, asks it to once.
Each agent is a folder in the agents folder (agents/ in the hq folder): its role is a git repo in agents/<name>/role/, its life (memory, data, files) is next to it. Whoever changes it (you, a code agent, or the agent itself writing a learned skill) changes that checkout; every version that passes its checks is committed there automatically, so its history is the agent's history.
hq install <owner>/<repo> # or a github.com URL; .../tree/<ref>/<folder> for one agent in a bigger repo
hq install <link> --as <name> # under another name
hq install <link> --yes # without the question (the summary is always printed)
hq update <agent>|--all # pull upstream (fast-forward only; your edits are never overwritten)
hq history <agent> # every committed version
hq restore <agent> <commit> # bring one back, as a new commit (undo with another restore)
hq share <agent> [--public] # put it on GitHub as <you>/hq-agent-<name>, private unless --public
hq share <agent> --public --fresh # a public copy without its history, at <you>/hq-<name>
hq market [words] # agents people shared, most starred first: pick any by number to install
hq search [words] [--mine] # find agents: GitHub repos tagged hq-agent (yours incl. private, anyone's public)
hq deps <agent>|--all [--install] # programs an agent runs that this Mac lacks; --install adds them
hq push <agent>|--all # push committed versions (hq-state commit does this for every agent)
Installing shows what the agent would do on your Mac before it is kept: its permission mode, what it may run without asking, hooks, MCP servers, tools, skills, and the services it asks for (nothing is shared until you grant it). An agent may bring the definition of a service the kernel lacks in its own services/<name>.json. Programs it runs go in requires: in its capabilities.yaml (ffmpeg: brew install ffmpeg # cutting footage): installing lists the missing ones and offers to install them, hq doctor keeps checking, hq deps <agent> --install adds them later. --public runs the privacy check over the package and its whole history and refuses while either names you: a public repo shows every old commit. --fresh publishes a separate public copy instead: one commit per publish with the agent as it is now (its files still checked), while the agent's own repo keeps every version and stays private. Every share writes a README from agent.md unless the agent has one. An agent installed from someone else keeps their repo as upstream; hq share makes yours origin, so hq update still pulls their changes.
Agent repos carry the GitHub topic hq-agent (hq share adds it) and the kernel hq, so they group without an organization: hq search --mine lists yours, private ones included, and public agents from anyone show up in hq search and at github.com/topics/hq-agent. To publish an agent of your own made outside hq share, add the topic.
Running hq takes 3 + N repos: this kernel, your state repo, your agents repo, and one per agent role you've shared. Agents installed from someone else stay in their repo (+0 until you hq share your own copy); third-party skills are fetched from their authors' repos (+0). Several agents can share one repo, installed by folder (hq install <owner>/<repo>/tree/main/<folder>), though hq share makes one repo per agent.
| Repo | Holds | Visibility |
|---|---|---|
hq (this one) | the kernel: kernel/ (code, PROTOCOL.md, services/, the plugin), docs, install.sh | public |
<you>/hq-state, cloned to ~/.hq | hq's knowledge: user.md, projects, agents.json, private-words | private |
<you>/hq-agents, cloned to agents/ | each agent's life: memory, data (as text dumps), jobs, private skills | private |
<you>/hq-agent-<name>, in agents/<name>/role | one agent's role: agent.md, settings, skills, tools, learned skills | private unless you share it |
| You change | It goes to | Saved by |
|---|---|---|
| An agent (you edit it, ask it to change, or it learns a skill) | that agent's repo | committed automatically on its next run; pushed by hq-state commit or hq push <name> |
A new agent (hq-new, or ask the builder) | a new local repo in the agents folder | hq backup once creates its private GitHub repo; from then on as above |
The kernel or PROTOCOL.md | this repo | a normal commit and push; it's public, so hq-pkg check --share first |
A service (kernel/services/<name>.json) | this repo | as the kernel; it names its secrets, never their values |
| Memory and data | the agents repo | hq-state commit |
| Your profile, projects, which agents you have | the state repo | hq-state commit |
hq backup does all of it at once: a private repo for each piece that has none yet (it lists them and asks), then commits and pushes everything. Saying yes to backups in hq init also runs it every night at 03:00 (hq backup --nightly, a LaunchAgent; --nightly off stops it), so a new agent of yours gets its private repo the next night. hq doctor says in one line what is backed up, what changed since, and what is only on this Mac.
A kernel change reaches every agent on its next run without touching their repos. An agent that starts to need: a new service only passes its checks where that service exists, so push the service with the kernel.
On a new Mac, install as usual: hq init asks whether to bring back a GitHub backup (logging you in to GitHub if needed), finds <you>/hq-state, fetches it into ~/.hq and your hq-agents into agents/, then installs each agent's role (agents.json) into agents/<name>/role. It names any agent that was never backed up. install.sh --from <you> does the same without asking.
Not in any repo, so redo them by hand on a new Mac:
agents.json records it without a link, so install.sh skips it;~/.hq/secrets/auth.json), secrets (Keychain), ~/.hq/secrets/env: hq auth grant, hq auth set;~/Library/LaunchAgents/dev.hq.*.plist): hq schedule --install, hq-bridge --install, each tool's own install command. The jobs themselves are in the agents repo.agents/<name>/files/): on disk only; back it up with the rest of the Mac.Everything is in the hq folder, laid out by what each thing is (docs/architecture.md, Concepts): hq and each agent have what they are (shareable, versioned) and what they have lived (private). The hq folder is this repo; its code is in kernel/, which is what hq is. Each agent is a folder in agents/: role/ is what it is (its own repo), and next to it what it has lived: memory/, data/, files/. This repo ignores agents/ (its own private repo), so git pull only ever updates hq. Don't run git clean -x here: it deletes ignored folders. ~/hq is hq's stable address, a link to the hq folder (made by install.sh), so the folder can live anywhere, an external disk included; HQ_HOME points somewhere else instead.
~/.hq is this Mac's hq, a real folder on the internal disk, as ~/.claude is Claude Code's. What hq has lived (who it works for, projects, the team, rules) is its git repo (your hq-state); its .gitignore commits only those files, so everything else in it stays on this Mac: secrets/ (grants, tokens; agents' file tools are denied it), settings, job state, the cost ledger, role snapshots, logs/, cache/.
install.sh links ~/hq and ~/.local/bin/hq, makes ~/.hq, installs the Claude Code plugin
.claude-plugin/ this repo as a plugin marketplace (serves kernel/adapters/claude-code)
docs/ architecture, codebase map
kernel/ the code
adapters/claude-code/ the Claude Code adapter (plugin)
lib/hq_paths.py the one place that knows where things are
bin/project-id resolves the current directory to a project; --list, --register
bin/hq run an agent directly: hq ls · hq <agent> [-p ID] [task] · --print
bin/hq-pkg agents: install, update, history, restore, share; versions: resolve (pin a run), pin, check
bin/hq-new scaffold a new agent as its own repo
bin/hq-auth shared services: which agent may use which, as which account
bin/hq-state commit what was lived: state and agents repos (sqlite dumped as text), then each role
bin/hq-ask ask an agent and get its answer back; continue the conversation (-c)
bin/hq-remote agents on your other machines over ssh: hq remote add, hq ask agent@machine
bin/hq-memory search the agents' memory and shared notes
bin/hq-sched scheduled agent jobs (cron, launchd)
PROTOCOL.md memory rules, appended to every agent's prompt
services/<name>.json shared services (MCP servers, APIs, logins): how to start them, which secrets by name
agents/ the agents folder (ignored by this repo; your hq-agents repo)
<name>/ one agent
role/ what it is: its own git repo (shareable)
agent.md identity, how it works, skill index
capabilities.yaml tools and data sources it may use, by name only — never credentials
settings.json Claude Code settings merged over your global ones for hq runs: model, plugins, permissions
skills/ tools/ its skills (read on demand) and the tools they drive
skills/learned/ skills it wrote itself; versioned and shared with it
memory/MEMORY.md long-term index (auto-loaded on every call)
memory/projects/<id>.md its notes on one project
memory/sessions/<id>.md where the last call on that project left off
data/ its databases; committed as text dumps (<name>.sql), not binaries
files/<project>/ what it made: renders, images, exports, backups (on disk, not in git)
skills/learned/ private skills, never shared (hq-pkg promote <agent> <skill> moves one into role/)
jobs.json its scheduled jobs (hq schedule)
~/.hq/ this Mac's hq; what hq has lived is your hq-state repo
projects.json project IDs → the git remotes / paths that belong to them
agents.json which agents you have and where their roles come from
private-words words no shared role may contain (hq-pkg check --share)
user.md who hq works for (incl. visual taste), read by every agent
projects/<id>/ codebase.md (shared project map) + decisions.md (append-only log)
secrets/ … store/ logs/ this Mac only, never committed (snapshots of roles: store/<name>@<version>/)
An agent's package is its role/; its life is the folder around it. Every run is pinned to a snapshot of it, so agents can be upgraded at any time, by anyone (you, the agent writing a learned skill, a code agent like this one), without waiting for running sessions:
bin/hq-pkg fingerprints the package. Unchanged: run the existing snapshot. Changed: check it (frontmatter, JSON, skills named in agent.md exist, needed services exist), then snapshot it into ~/.hq/store/<name>@<version>/ (an APFS clone on the same disk, else hard links to the previous one's unchanged files), and commit it to the agent's repo. Prompt and settings stay templates in the snapshot; placeholders are filled at run time. Running sessions keep the snapshot they started on.{{PLACEHOLDERS}}, skills named in agent.md exist, needed services exist.touch <agents folder>/<name>/.editing first and delete it when done; runs keep the last good version meanwhile, so nobody gets a half-edited agent. A marker older than an hour is ignored.hq versions <name> lists them; hq rollback <name> pins the previous one; hq pin <name> <version>; hq unpin <name> follows edits again. Old snapshots are removed after two days unless pinned, good or running; the commits stay, and hq restore brings any of them back.hq:<name> subagents are pinned per Claude Code session (the adapter resolves every agent at session start).In prompts and settings: {{AGENT_DIR}} is the pinned package (read skills and run tools from it), {{PACKAGE}} the agent's live role/ (where it writes learned skills), {{AGENTS}} the agents folder (another agent's tools: {{AGENTS}}/house/role/tools/house.py), {{HOME_DIR}} the agent's own folder (memory, data, files), {{HOME_REAL}} the same with symlinks resolved (use it in permission rules: Claude Code matches resolved paths), {{STATE}} the state folder, {{HQ}} the kernel repo, {{USER_HOME}} your home folder (for permission rules, instead of /Users/<name>). Tools find their data through the snapshot's .home file; run from the live role/ (launchd, another agent), they use the folder around it.
Skills from public GitHub repos are linked, not copied. An agent's skills.json names each one's repo, path and ref (a full commit sha) and its licence; hq-pkg fetch <agent>|--all downloads just those folders into skills/<name>/ (a shallow, sparse fetch: seconds, however big the repo), where they're git-ignored and carry a .source file. install.sh fetches every agent's; a run fetches anything missing on first use. A package whose linked skills aren't fetched fails its checks, so the last good version runs. To upgrade one, change its ref. Notes of your own about a linked skill go next to it (image's skills/anatomy-hq.md), never inside it. A skill for your own install only (one you can't or won't share) goes in the agent's own skills/learned/ (next to role/, not in it), not the package, listed in its MEMORY.md.
Three layers, so sharing an agent never shares an account:
| Layer | Where | Says |
|---|---|---|
| Service | kernel/services/<name>.json (repo) | what it is (mcp, cli, login), how to start it, which env vars it needs |
| Need | needs: [...] in the agent's capabilities.yaml (package) | what the agent asks for |
| Grant | ~/.hq/secrets/auth.json (yours, never in git) | profiles (an account: where each secret comes from) and grants (agent → service → profile) |
hq auth # who needs what, who has what, whether each secret resolves
hq auth grant personal notion notion/library
hq auth set notion/library NOTION_API_KEY # store in Keychain (prompts), point the profile at it
bin/hq starts only the MCP services an agent needs and is granted; secrets are resolved when the server starts (hq-auth exec) and exist only in that process, never in a config file or the agent's environment. A cli tool asks hq-auth get --for <agent>, which refuses agents without a grant. Every secret handed out is logged (time, service, profile, who asked, who was running; never the value) to ~/.hq/secrets/auth.log. A broken auth.json or a secret that doesn't resolve costs the agent that service for the run, never the run itself. Profile sources: keychain:SVC#ACCOUNT, env-file:PATH#VAR, env:VAR. This keeps cooperating agents to least privilege; it isn't a sandbox (an agent with a shell can read what your user can). hq:<name> subagents run inside the calling session and share its MCP servers; give an agent with its own credentials a full hq <name> run when it needs them.
hq <agent> runs a normal Claude Code session: your global settings, hooks, skills and the repo's own config all load. The agent's settings.json (rendered into the snapshot) is merged on top to narrow it per agent: all agents turn off Claude Code's own auto memory (hq memory replaces it) and the flow plugin (flow calls agents, not the other way round); designer links impeccable as a pinned skill, devops keeps vercel; finance and personal can't edit ~/.hermes. Subagent calls (hq:<name> inside a session) use that session's settings instead.
Model and effort live in agent.md's frontmatter (model: sonnet, effort: low|medium|high|xhigh|max), so they apply to both hq <agent> runs and hq:<name> subagents and change with the agent's version. Override one run with hq <agent> -m MODEL -e EFFORT (or HQ_MODEL / HQ_EFFORT); leave a field out to use your global setting.
bin/hq-cost keeps every Claude Code request's cost (list API prices) in ~/.hq/cost.db, so it outlives transcript cleanup. Each session hq starts is tagged with the agent, package version, model, effort and caller (terminal or discord); hq:<name> subagents are recognised by their type. hq cost agents [today|week|month|all] compares agents; hq cost agent video shows one agent by day, by version/model/effort and its costliest runs; hq-cost csv exports every request with its agent for analysis elsewhere.
hq ask house "what expires this week?" runs the agent exactly as hq house "…" does (its own package, settings, services, memory, project) but prints only its answer and keeps the session: hq ask house -c "and next week?" continues the same conversation from the same directory, --as NAME names one to continue from anywhere, and a continued conversation keeps its model and effort. --json gives answer, session and cost for programs; hq ask ls lists the conversations. A Claude Code session can use it to work with a specialist over several turns in the specialist's own tools, where an hq:<name> subagent would run in the caller's. Tagged via=ask in the cost ledger.
An agent lives on the machine that has its tools. On a l
hooks/register.tsx 317 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// Claude Code adapter for hq. The kernel (bin/, PROTOCOL.md) lives in the hq repo; the installed agents in the
4// agents folder (<hq>/agents/<name>/role, each its own git repo, next to the agent's memory, data and files; listed by bin/hq-pkg); this plugin only reads them:
5// session.start register every installed agent's agent.md as `hq:<name>`, pinned for this session to the
6// package version bin/hq-pkg resolves (edits made meanwhile apply to the next session)
7// agent.spawn resolve the project, prepend it and the agent's memory to the task
8// turn.step the agent's `effort:` (agent.md frontmatter) on its model requests; `model:` is set at register
9// classic.SubagentStop nudge once if the agent didn't write its session file
10// /hq, mcp__hq__invoke list agents; call one with an explicit project
11// The kernel comes from hq_path (default ~/hq/kernel, else ~/.hq/kernel, else ~/Workspace/hq); agents and state from the resolver, as every
12// kernel tool finds them (lib/hq_paths.py).
13
14type Spawned = { agent: string; project: string; startedMs: number; isNudged: boolean }
15
16const spawned = new Map<string, Spawned>()
17// Reasoning effort per hq agent (from agent.md) and per running hq subagent. A spawn has no effort field, so
18// agent.spawn notes the agent and turn.step applies it to that agent's requests.
19const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max'])
20const agentEffort = new Map<string, string>()
21const runningEffort = new Map<string, string>()
22
23function frontmatter(text: string): { fields: Record<string, string>; body: string } {
24 const m = /^---\n([\s\S]*?)\n---\n?/.exec(text)
25 if (!m) return { fields: {}, body: text }
26 const fields: Record<string, string> = {}
27 for (const line of m[1].split('\n')) {
28 const kv = /^([A-Za-z_-]+):\s*(.*)$/.exec(line)
29 if (kv) fields[kv[1]] = kv[2].trim()
30 }
31 return { fields, body: text.slice(m[0].length) }
32}
33
34async function hqRoot($: EngineInterface, configured: unknown): Promise<string> {
35 const set = typeof configured === 'string' ? configured.trim() : ''
36 const home = (await $.env.get('HOME')) ?? ''
37 if (set) return set.replace(/^~(?=\/)/, home)
38 // ~/hq/kernel is where install.sh puts it; ~/.hq/kernel and ~/Workspace/hq are where older installs have it.
39 for (const k of [`${home}/hq/kernel`, `${home}/.hq/kernel`]) if (await $.fs.exists(`${k}/bin/hq-pkg`)) return k
40 return `${home}/Workspace/hq`
41}
42
43// Where things are, from the kernel's one resolver (lib/hq_paths.py): the state folder (~/.hq) and
44// agents/, each as an address and with symlinks resolved (_REAL, what permission rules are matched against).
45async function hqPaths($: EngineInterface, hq: string): Promise<Record<string, string>> {
46 const run = await $.process.run(['/usr/bin/python3', `${hq}/lib/hq_paths.py`, '--json'], { timeoutMs: 10_000 })
47 try {
48 if (run.exitCode === 0) return JSON.parse(run.stdout) as Record<string, string>
49 } catch {}
50 return { HQ_STATE: `${hq}/state`, HQ_AGENTS: `${hq}/agents`, HQ_STATE_REAL: `${hq}/state`, HQ_AGENTS_REAL: `${hq}/agents` }
51}
52
53// Helpers that take $ must be top-level functions: the hooks loader refuses the whole module otherwise.
54
55// Installed agents, {name: package folder}: the agents folder first, then the repo's own agents/ (older installs).
56async function listAgents($: EngineInterface, hq: string): Promise<Record<string, string>> {
57 const run = await $.process.run([`${hq}/bin/hq-pkg`, 'list', '--json'], { timeoutMs: 30_000 })
58 try {
59 const out = run.exitCode === 0 ? (JSON.parse(run.stdout) as Record<string, { dir: string }>) : {}
60 return Object.fromEntries(Object.entries(out).map(([name, o]) => [name, o.dir]))
61 } catch {
62 return {}
63 }
64}
65
66// One call pins every agent: {name: {dir, version, note}}. Empty when hq-pkg fails; agents then run live.
67async function resolvePackages($: EngineInterface, hq: string): Promise<Record<string, { dir: string; note: string }>> {
68 const run = await $.process.run([`${hq}/bin/hq-pkg`, 'resolve', '--all', '--json'], { timeoutMs: 30_000 })
69 try {
70 return run.exitCode === 0 ? (JSON.parse(run.stdout) as Record<string, { dir: string; note: string }>) : {}
71 } catch {
72 return {}
73 }
74}
75
76// One line on where this session sits among the user's projects (bin/project-id --info, --children), or '' for a
77// project with no parent, home or children. In a repo: its parents and their files. At a parent: what it holds.
78async function projectContext($: EngineInterface, hq: string, cwd: string): Promise<string> {
79 const run = async (...args: string[]) => {
80 const r = await $.process.run([`${hq}/bin/project-id`, ...args], { timeoutMs: 10_000 })
81 return r.exitCode === 0 ? r.stdout.trim() : null
82 }
83 const id = await run(cwd)
84 if (!id) return ''
85 const rows = (out: string | null) => (out ? out.split('\n').map((l) => l.split('\t')) : [])
86 const chain = rows(await run('--info', id))
87 const held = rows(await run('--children', id))
88 const homes = chain.filter(([, h]) => h && h !== '-').map(([p, h]) => `${p}: ${h}`)
89 if (chain.length < 2 && !homes.length && !held.length) return ''
90 const parents = chain.slice(1).map(([p]) => p)
91 return (
92 `hq project: ${id}` + (parents.length ? `, part of ${parents.join(' → ')}` : '') + '.' +
93 (held.length
94 ? ` It holds ${held.map(([c, d]) => `${c} (${d === '-' ? 'not on this Mac' : d})`).join(', ')}.`
95 : '') +
96 (homes.length ? ` The user's own files for it (specs, briefs, contracts): ${homes.join('; ')}.` : '') +
97 (parents.length
98 ? ' Open the parent\'s files only when the task needs more than this project; `hq project` shows the rest.'
99 : ' `hq project` shows the rest.')
100 )
101}
102
103export const register: Register = (on, options) => {
104
105 // agentDir: the pinned snapshot from hq-pkg (or the live folder when versioning is unavailable).
106 // state: the state folder; stateReal: the same with symlinks resolved ({{HOME_REAL}}, used by permission rules).
107 let state = ''
108 let stateReal = ''
109 let userHome = ''
110 let agentsRoot = ''
111 let machine = ''
112 let installed: Record<string, string> = {}
113 let agentsReal = ''
114 let projectLine = ''
115 // An agent's life (memory, data, files): its folder next to role/; older layouts: <state>/agents/<name>.
116 const lifeOf = (agent: string, hq: string, real = false) => {
117 const dir = installed[agent]
118 if (dir?.endsWith('/role')) return real && agentsReal ? `${agentsReal}/${agent}` : dir.slice(0, -'/role'.length)
119 return `${(real && stateReal) || state || hq}/agents/${agent}`
120 }
121 const fill = (text: string, hq: string, agent: string, agentDir = installed[agent] ?? `${hq}/agents/${agent}`) =>
122 text
123 .replaceAll('{{AGENT_DIR}}', agentDir)
124 .replaceAll('{{AGENTS}}', agentsRoot || `${state || hq}/packages`)
125 .replaceAll('{{PACKAGE}}', installed[agent] ?? `${hq}/agents/${agent}`)
126 .replaceAll('{{HOME_DIR}}', lifeOf(agent, hq))
127 .replaceAll('{{HOME_REAL}}', lifeOf(agent, hq, true))
128 .replaceAll('{{STATE}}', state || hq)
129 .replaceAll('{{MACHINE}}', machine || `${userHome}/.hq`)
130 .replaceAll('{{USER_HOME}}', userHome)
131 .replaceAll('{{HQ}}', hq)
132 .replaceAll('{{AGENT}}', agent)
133
134 on('session.start', async ($, e, next) => {
135 const result = await next(e)
136 const hq = await hqRoot($, options.hq_path)
137 if (!(await $.fs.exists(`${hq}/bin/hq-pkg`))) {
138 $.ui.toast(`hq: no hq repo at ${hq}; set hq_path in /plugin config`)
139 return result
140 }
141 const protocol = await $.fs.read(`${hq}/PROTOCOL.md`)
142 const where = await hqPaths($, hq)
143 state = where.HQ_STATE
144 stateReal = where.HQ_STATE_REAL
145 agentsRoot = where.HQ_AGENTS
146 machine = where.HQ_MACHINE ?? ''
147 agentsReal = where.HQ_AGENTS_REAL
148 installed = await listAgents($, hq)
149 userHome = (await $.env.get('HOME')) ?? ''
150 projectLine = await projectContext($, hq, e.cwd)
151 const packages = await resolvePackages($, hq)
152 for (const [name, dir] of Object.entries(installed)) {
153 const file = `${dir}/agent.md`
154 if (!(await $.fs.exists(file))) continue
155 const pkg = packages[name]
156 // The pinned agent.md, so model and effort change with the version like everything else.
157 const pinned = pkg && (await $.fs.exists(`${pkg.dir}/agent.md`)) ? `${pkg.dir}/agent.md` : file
158 const { fields, body } = frontmatter(await $.fs.read(pinned))
159 if (EFFORTS.has(fields.effort ?? '')) agentEffort.set(name, fields.effort)
160 else agentEffort.delete(name)
161 const rendered = pkg && (await $.fs.exists(`${pkg.dir}/prompt.md`)) ? await $.fs.read(`${pkg.dir}/prompt.md`) : ''
162 await $.agent.register({
163 name,
164 description: fields.description ?? `hq specialist: ${name}`,
165 prompt: rendered
166 ? fill(rendered, hq, name, pkg.dir)
167 : `${fill(body, hq, name)}\n${fill(protocol, hq, name)}`,
168 ...(fields.model ? { model: fields.model } : {}),
169 })
170 }
171 await $.command.register({
172 name: 'hq',
173 description: 'hq specialists: list them, or delegate a task (/hq <agent> [-p project] <task>)',
174 argumentHint: '[agent] [-p project] [task]',
175 })
176 await $.tool.register({
177 name: 'invoke',
178 description:
179 'Call an hq specialist (persistent agent with its own memory) on a task. ' +
180 'Give `project` when the work is for a known project that the current directory does not belong to.',
181 inputSchema: {
182 type: 'object',
183 properties: {
184 agent: { type: 'string', description: 'Agent name, e.g. branding' },
185 task: { type: 'string', description: 'What to do' },
186 project: { type: 'string', description: 'Project ID from projects.json (optional)' },
187 },
188 required: ['agent', 'task'],
189 },
190 })
191 return result
192 })
193
194 // Every hq spawn, whoever starts it: project + fresh long-term memory go in front of the task.
195 // A session in a project that sits in a parent, or has a home, is told so in one line; nothing for any other.
196 on('prompt.compose', async ($, e, next) => {
197 const result = await next(e)
198 if (!projectLine) return result
199 return { sections: [...result.sections, { id: 'hq:project', text: projectLine, scope: 'session' as const }] }
200 }).catch(($, e, next) => next(e))
201
202 on('agent.spawn', async ($, e, next) => {
203 if (!e.subagentType.startsWith('hq:')) return next(e)
204 const hq = await hqRoot($, options.hq_path)
205 const agent = e.subagentType.slice(3)
206 const cwd = e.cwd ?? (await $.session.cwd())
207
208 const named = /^<hq project="([a-z0-9][a-z0-9._-]*)"\/>\n?/.exec(e.prompt)
209 let project = named?.[1] ?? ''
210 if (!project) {
211 const found = await $.process.run([`${hq}/bin/project-id`, cwd], { timeoutMs: 10_000 })
212 project = found.exitCode === 0 ? found.stdout.trim() : ''
213 }
214 const task = named ? e.prompt.slice(named[0].length) : e.prompt
215 // The project's parents, closest first (bin/project-id --info): agents read their memory too.
216 const info = project ? await $.process.run([`${hq}/bin/project-id`, '--info', project], { timeoutMs: 10_000 }) : null
217 const parents = info?.exitCode === 0
218 ? info.stdout.trim().split('\n').slice(1).map((l) => l.split('\t')[0]).join(' ')
219 : ''
220 if (!state) state = (await hqPaths($, hq)).HQ_STATE
221 const memoryFile = `${lifeOf(agent, hq)}/memory/MEMORY.md`
222 const memory = (await $.fs.exists(memoryFile)) ? await $.fs.read(memoryFile) : ''
223 const today = new Date().toISOString().slice(0, 10)
224
225 const prompt =
226 `<session project="${project || 'none'}"${parents ? ` parents="${parents}"` : ''} cwd="${cwd}" date="${today}"/>\n` +
227 `<long-term-memory path="${memoryFile}">\n${memory}\n</long-term-memory>\n\n${task}`
228
229 const started = await next({ ...e, prompt })
230 if (started.agentId) {
231 spawned.set(started.agentId, { agent, project, startedMs: Date.now(), isNudged: false })
232 const effort = agentEffort.get(agent)
233 if (effort) runningEffort.set(started.agentId, effort)
234 }
235 return started
236 }).catch(($, e, next) => next(e)) // never block a spawn because of hq
237
238 // Only requests that carry an effort (the model supports it) get the agent's.
239 on('turn.step', async function* ($, e, next) {
240 const effort = e.agentId !== undefined ? runningEffort.get(e.agentId) : undefined
241 if (effort === undefined || e.effort === undefined) return yield* next(e)
242 return yield* next({ ...e, effort: effort as typeof e.effort })
243 })
244
245 // The session file is how the next call picks up; ask once if it wasn't written.
246 on('classic.SubagentStop', async ($, e, next) => {
247 const run = spawned.get(e.agent_id)
248 if (!run || !run.project || run.isNudged || e.stop_hook_active) return next(e)
249 const hq = await hqRoot($, options.hq_path)
250 if (!state) state = (await hqPaths($, hq)).HQ_STATE
251 const file = `${lifeOf(run.agent, hq)}/memory/sessions/${run.project}.md`
252 const isWritten = (await $.fs.exists(file)) && (await $.fs.stat(file)).mtimeMs >= run.startedMs
253 if (isWritten) {
254 spawned.delete(e.agent_id)
255 runningEffort.delete(e.agent_id)
256 return next(e)
257 }
258 run.isNudged = true
259 return {
260 block:
261 `Before you finish: write ${file} (task, state, next step, open questions; max 30 lines), ` +
262 'append any decisions, and update project or long-term memory if you learned something durable.',
263 }
264 }).catch(($, e, next) => next(e))
265
266 on('command.run', { command: 'hq' }, async ($, e) => {
267 const hq = await hqRoot($, options.hq_path)
268 const words = e.args.trim().split(/\s+/).filter(Boolean)
269 if (!Object.keys(installed).length) installed = await listAgents($, hq)
270 const agents = Object.keys(installed).sort()
271
272 if (words.length === 0) {
273 const project = await $.process.run([`${hq}/bin/project-id`, await $.session.cwd()])
274 return {
275 text:
276 `hq agents: ${agents.join(', ')}\n` +
277 `project here: ${project.exitCode === 0 ? project.stdout.trim() : 'none'}\n` +
278 'Delegate with /hq <agent> [-p project] <task>.',
279 }
280 }
281
282 const [agent, ...rest] = words
283 if (!agents.includes(agent)) return { text: `hq: no agent '${agent}'. Agents: ${agents.join(', ')}` }
284 let project = ''
285 if (rest[0] === '-p') {
286 project = rest[1] ?? ''
287 rest.splice(0, 2)
288 }
289 const task = rest.join(' ')
290 if (!task) return { text: `hq: what should ${agent} do? /hq ${agent} [-p project] <task>` }
291 return {
292 context: [
293 `The user asked to delegate this to the hq specialist \`hq:${agent}\`. Call the Agent tool with ` +
294 `subagent_type "hq:${agent}" and this prompt, word for word:\n\n` +
295 (project ? `<hq project="${project}"/>\n` : '') +
296 task +
297 '\n\nWhen it reports back, relay what it did and anything it needs from the user.',
298 ],
299 }
300 })
301
302 on('tool.call', { tool: 'mcp__hq__invoke' }, async ($, e) => {
303 const input = e as unknown as { agent: string; task: string; project?: string }
304 const prompt = (input.project ? `<hq project="${input.project}"/>\n` : '') + input.task
305 const started = await $.agent.spawn({
306 subagentType: `hq:${input.agent}`,
307 description: `hq ${input.agent}`,
308 prompt,
309 })
310 return {
311 result: started.deny
312 ? `hq: could not start ${input.agent}: ${started.deny}`
313 : `hq:${input.agent} started (agent ${started.agentId}); its report arrives when it finishes.`,
314 }
315 }).catch(() => ({ result: 'hq: invoke failed inside the hq plugin.' }))
316}
317