Imported from soundtrip-health/kuhn (
AGENTS.md). Install upstream withnpx skills add soundtrip-health/kuhn. Copyright stays with the author.
CLAUDE.md
Guidance for Claude Code (and human contributors) working on the Kuhn codebase. For what the product is and how to use it, see README.md and docs/architecture.md — don't duplicate that here.
CLAUDE.mdis a symlink toAGENTS.md. EditAGENTS.md.
Repository layout
This is a monorepo of two independently-installed Node packages plus supporting
content. The root package.json is a dev-only orchestrator (a dev script that
runs both apps via concurrently, and a postinstall that installs both packages) —
it holds no app code and publishes nothing. Each package is still installed and run
independently, so npm commands (test, build, db:seed, …) run inside each
package; only npm install and npm run dev are meaningful at the root.
kuhn/
├── agent-backend/ # Node.js service: agent runtime, REST + WebSocket, SQLite, Yjs, render/export
├── webapp/ # Browser app (Vite + TypeScript): chat, Milkdown/Crepe editor, file manager
├── docs/ # architecture.md, deployment.md, data-pipeline.md, design/
├── guidance-docs/ # Kuhn knowledge catalog (issue #65): catalog.json + curated reference corpus, DB-seeded at startup
├── shared-scripts/ # Kuhn shared-script catalog (issue #68): catalog.json + known-good analysis scripts, DB-seeded at startup
├── slide-themes/ # Kuhn Marp slide-theme catalog (STH-58): catalog.json + theme CSS, DB-seeded at startup
├── typst-templates/ # Kuhn Typst page-layout catalog: catalog.json + .typ templates (NIH, manuscript) + their Word reference .docx (tools/make-reference-docx.py), DB-seeded at startup
├── doc-types/ # Kuhn document-type catalog (issue #106): catalog.json — slug, title, wizard hints, per-type agent guidance; org owners extend/shadow it in Org admin → Document types; DB-seeded at startup
└── test-projects/ # end-to-end test-project fixtures (wizard answers + prompts + data prep) — see test-projects/README.md
Agent definitions (system prompts, models, tools) are DB-seeded from
agent-backend/src/db/prompts/*.md + seed-data.js — see "Agent prompts" below.
Running the apps
Both run locally; the webapp talks to the backend over REST + WebSocket.
Backend (agent-backend/) — port 3002
cd agent-backend
npm install # first run
npm run dev # node --watch src/index.js
The database is in-process SQLite — no service to run. On startup it creates the DB file, applies the schema, and seeds agents/tools. Health: http://localhost:3002/health
npm test/npm run test:watch— vitestnpm run db:seed— applyschema.sql+ migrations, then re-seed agents, tools and catalogs (run after editing prompts/seed data, or after pulling a release that adds tables, without restarting the server)npm run smoke— research smoke test (uses real model quota)
The backend also serves webapp/dist at / whenever that build exists
(single-port deployment — see docs/deployment.md; disable with
KUHN_WEBAPP_DIST=''). Production webapp builds call the API on their own
origin; dev builds default to http://localhost:3002.
Needs ANTHROPIC_API_KEY in agent-backend/.env. The SQLite DB and uploaded
project files both live under an explicit data directory, KUHN_DATA_DIR
(default: repo-root ./data, gitignored) — data/db/kuhn.sqlite and
data/files/<projectId>/. Override the DB path alone with KUHN_SQLITE_PATH,
or the file root with PROJECTS_ROOT. Render/export shell out to sandboxed
Typst/Pandoc Docker images
(docker pull pandoc/core:latest minidocks/poppler:latest — poppler powers org-library PDF ingestion, story 006-002; marp renders slide decks, STH-57). The Typst renderer is built: docker build -t kuhn/typst:latest docker/typst (the official image plus the metric-compatible fonts the page-layout templates name — Liberation Sans for Arial etc.; the stock ghcr.io/typst/typst image works via SANDBOX_TYPST_IMAGE, but NIH/journal templates then fall back to Libertinus and page counts drift). The analyst's run_script R runtime is also built: docker build -t kuhn/r-analysis:latest docker/r-analysis (issue #68b; packages are baked in because the sandbox has no network). So is the marp slide renderer: docker build -t kuhn/marp:latest docker/marp (STH-61; adds LibreOffice for editable pptx — the pulled marpteam/marp-cli image works too, minus editable pptx).
The local data directory is disposable. There is no production data in a
dev checkout: every project under data/ is a test project. Delete
data/db/kuhn.sqlite and data/files/ whenever it is convenient — the backend
reapplies schema.sql and re-seeds agents/tools on startup — and create,
modify or delete projects freely. The token-free check scripts write into
projects[0] by design (override with PROJECT_ID); that needs no permission
and no warning. They still purge their own fixtures, only so that repeated runs
stay readable.
Webapp (webapp/) — port 5174 (pinned)
cd webapp
npm install # first run
npm run dev # vite (backend must be running)
The port is pinned because the backend CORS allowlist hard-codes it. Build with
npm run build (tsc && vite build — type errors fail the build).
Token-free check scripts (drive the app without spending model quota):
npm run smoke, editor-check, parity-check, smoke:chat, write-check, models-check, stale-room-check (self-contained: starts its own isolated backend + vite pair and replays the collab-room duplication incident).
Where things live
agent-backend/src/
index.js— server entry (Express + ws);config.js— env/configroutes/— REST handlers;session.js— agent chat sessionsagents/— agent runtime (therunAgentTaskboundary, Claude Agent SDK, tool dispatch, project seeding pipeline)db/+db.js— SQLite access (better-sqlite3;db.jskeeps a$1-placeholder,{rows}-returning shim):schema.sql(DDL),prompts/*.md+seed-data.js(agent/tool/reference seed data),seed.js(applies it),init.js(startup: schema → seed),references.js(per-project reference store + .bib export),memory.js(shared project memory, issue #150: immutable keyed entries + FTS recall;agents/memory-context.jsrenders the prompt digest,agents/tools/memory.jsthe remember/recall/forget tools),org-secrets.js(encrypted org secrets store — values write-only, resolved server-side)storage.js— project-scoped file API (enforces the project root — all file access goes through here)sandbox.js— sandboxed subprocess execution;render.js— markdown → Typst → PDF, Pandoc exportyjs-websocket.js/yjs-signaling.js— real-time collab servers*.test.js— colocated vitest tests
webapp/src/ — flat TS modules, one concern each: main.ts (entry), chat.ts,
editor.ts (Milkdown/Crepe), files.ts, project-browser.ts, preview.ts,
api.ts (backend client), citation.ts/cite-picker.ts/bib.ts (/cite),
page-breaks.ts (page lines from the render's page map), page-break-chip.ts (\newpage chips), seeding.ts, plus style.css / kuhn-tokens.css for the design system.
Agent prompts (db/prompts/ + db/seed-data.js)
The seven agents (pm, writer, ra, advisor, reviewer, analyst, help) have their system
prompts in agent-backend/src/db/prompts/<slug>.md (plain markdown, no
escaping). Their names/models, the tool definitions, the agent→tool matrix, and
the default-tenant rows live in db/seed-data.js. seed.js applies both via
idempotent parameterized upserts; init.js runs it at startup (after
schema.sql) and npm run db:seed re-applies it. The runtime loads prompts
from the agents table.
- To change a prompt: edit
db/prompts/<slug>.md, thennpm run db:seed. - To change a model, tool, or assignment: edit
db/seed-data.js, thennpm run db:seed.
(Historical note: prompts lived in a top-level agents/ tree, then in a single
dollar-quoted seed.sql. The Postgres→SQLite move retired dollar-quoting, so
prompts returned to per-agent .md files — now under db/prompts/.)
Feature guide (docs/features/) — keep it current
docs/features/*.md is the user-facing feature guide the in-app help agent answers
from (issue #170). The backend indexes it at startup (FTS5, db/guide.js); the help
agent's only tool searches it. Any PR that adds or changes user-facing behaviour — a
button, a panel, a slash command, a front-matter key, a role requirement, a render or
restart prerequisite — updates the relevant page in the same PR. docs/features/README.md
defines the page format. Two tests keep it honest: agent-backend/src/db/guide.test.js
(every agent and every key in render.js FRONT_MATTER_KEYS is documented) and
webapp/src/slash-commands.test.ts (every slash command is documented). Add a new
front-matter parser to FRONT_MATTER_KEYS; add a new slash command to
webapp/src/slash-commands.ts.
Stories — project-management rules
Public work is tracked via GitHub issues and PRs. The maintainers additionally keep an
epic/story planning record in docs/epics/NNN-epic-slug/ (index.md + stories/) —
not part of the public tree; it lives in a private companion repository and in
maintainers' checkouts (git-excluded). If docs/epics/ is absent in your checkout,
skip this section. Statuses: draft, ready, in-progress, done, blocked.
These rules are about keeping that record honest:
- A
donestory is read-only — historical record, never the home for open work. - Every known issue has an owning open story — self-contained enough to act on without reading back into the done story.
- Done stories use forward pointers, not issue detail (e.g. "Deferred to Story 009"); the open story owns the full description.
- Marking a story
donerequires an issue audit — acceptance criteria met or explicitly deferred with a forward reference; every known issue has a receiving open story listed in the epic table.
Conventions
- Match the surrounding code — both apps are plain ESM; the webapp is dependency-light TS with no framework. Keep modules small and single-purpose.
- All project file access goes through
storage.js— never read/write a project path directly; the project-root enforcement is a tenancy-safety invariant. - Sandboxed execution only for render/export — go through
sandbox.js. - Tests are colocated (
*.test.js) and run with vitest.
Claude Code permissions
The whole .claude/ directory is gitignored — the repo ships no shared permission
allowlist, so each checkout starts from Claude Code's defaults and grants are personal.
Pre-allow the low-risk commands you get prompted for repeatedly (git read ops,
npm test/build/lint, node, python3, grep/rg, file tools) in either
.claude/settings.json (this checkout only) or ~/.claude/settings.json (all your
projects). Neither is committed. Toggle modes with /permissions.