Thanks for considering a contribution. This document is for maintainers and contributors to the npm package itself, not for end users of the tool. End-user docs live on the docs site.
Prerequisites:
- Node 22+
- npm 10+ (or pnpm 9+)
- Claude Code CLI on PATH for integration smoke tests (
claude --version) - No other external binaries. Capture does no secret scanning.
Git hooks are managed by husky and installed automatically by npm install (via the prepare script). The pre-commit hook runs lint-staged, which in turn runs ESLint and Prettier on staged files (plus index rebuild --stage when knowledge base nodes change), followed by typecheck and test across the project. The hook does not run typecheck:tests, so run it yourself before pushing; CI does.
Set up:
git clone git@github.com:e0ipso/kenkeep.git
cd kenkeep
npm install
npm run buildnpm install runs prepare, which only installs the husky git hooks. npm run build builds the CLI (build:cli) and copies the templates (build:templates); npm test and prepublishOnly run it first. After build, node dist/cli.js --help should work from the repo root.
src/
cli.ts # commander entry, registers subcommands
commands/ # one file per subcommand (init, doctor, status, ...)
harnesses/
types.ts # harness-agnostic adapter contract
registry.ts # central HarnessAdapter registry
detect.ts # env-based active-harness resolver
claude/ # Claude Code adapter
codex/ # OpenAI Codex CLI adapter
cursor/ # Cursor agent adapter
opencode/ # OpenCode adapter (TS plugin + per-event kk-hooks)
copilot/ # GitHub Copilot CLI adapter (per-event JSON hooks + kk-hooks)
lib/ # shared utilities (paths, log, version, schemas, ...)
templates-source/ # source for the shipped templates/ directory
scripts/
build-templates.mjs # copies templates-source/ to templates/, builds hook scripts
templates/ # built; bundled into the npm package
tests/
fixtures/ # transcripts and bootstrap docs used by integration tests
harnesses/ # per-adapter test suites
docs/ # Jekyll/Just-the-Docs site, served via GitHub Pages
PRD.md # product requirements (authoritative)
npm test # unit + integration with mocked `claude` subprocess
npm run typecheck # tsc --noEmit, production sources
npm run typecheck:tests # tsc -p tsconfig.test.json, src + tests + scripts/*.ts + vitest.config.ts
npm run lint # eslint
npm run format:check # prettierBoth type gates inherit the strict flags in tsconfig.json, including exactOptionalPropertyTypes and noUncheckedIndexedAccess. A test that does not typecheck fails CI the same as production code. scripts/*.mjs are not type-checked. CI (.github/workflows/test.yml) builds once, then runs npm test --ignore-scripts, both type gates, and lint. Local npm test keeps its pretest build.
Before a significant release (schema bump, capture/curate/consume behavior change, pinned Claude Code CLI bump), run these checks and record the results in the release PR. They cover what automation cannot: real sessions, other operating systems, and judgment about capture quality.
Sandbox: npm pack the candidate build, which writes kenkeep-<v>.tgz. In an empty directory run git init, npm init -y and npm install --no-save <path>/kenkeep-<v>.tgz, then npx --no-install kenkeep init --harnesses claude and npx --no-install kenkeep doctor (exit 0, warnings allowed). Use npx --no-install kenkeep for every later check so it runs the candidate, never the published package.
- Platform smoke. On macOS, Linux, WSL2, and native Windows: one
Stopcapture produces one_sessions/log withproposal_status: pending. On Windows, hook scripts must be LF and commands must use forward slashes. - PreCompact timing. Drive a session past auto-compact. Capture adds under 1 s, and the log holds the full transcript slice, not a summary.
time node .ai/kenkeep/hooks/claude/kk-capture.cjs < /dev/nullshould stay under 200 ms cold. - End-to-end. Ten to fifteen substantive messages, end the session, open a new one, run
/kk-curate. Expect one to four nodes, and judge whether they are the right facts (target 80 percent acceptance). Commit some,git restorethe rest, runindex rebuild, then ask the assistant what it knows about the project. init --upgrade. From the last published version with an editedproposal-extract.mdand a customconfig.yamlkey: the edit and the key survive, hook scripts andinstalled-versionshow the new version,doctorexits 0.logs prune. Backdate one JSONL withtouch -d "60 days ago". With the defaultlogsRetentionDays: 30, prune deletes it and keeps a fresh JSONL, and a second run reportspruned 0 files.logsRetentionDays: 0is refused with a validation error naming the key and deletes nothing./kk-bootstrap. On a small public repo, nodes land in topical folders, the summary lists skipped collisions, and no node carries a secret or a stale TODO. Re-running skips every unchanged doc by hash and reprocesses only an edited one.- Concurrency. Two parallel
curatelaunchers both finish without a lock error,state.jsonand every session log still parse, and unstamped sessions reprocess on the next run. Two rapidSessionStartdrains: the second skips while the first holds the lock, and a killed drain's lock is reclaimed within about a minute. - Settings. No
config.yamluses the defaults.curationThreshold: 3is honored. An unknown key fails with an error naming the file. - Doctor exit codes. Deleted
installed-versionis an error (exit 1). A danglingkk_derived_from, a hand-edited node after curate, or a version mismatch each warn (exit 0).
If a manual check finds a regression automation should have caught, add the missing test in the fix PR.
Run this before bumping the Version: of src/templates-source/prompts/proposal-extract.md or knowledge-admission.md. Prompts reach the model through the shared headless runner and substitute transcripts and candidates literally:
npm run prompt-eval -- --harness <id>It runs one headless call per fixture in tests/fixtures/prompt-eval/ through the selected adapter (24 generation calls plus up to 13 judge calls, two at a time), validates every result against the schema, and prints a Markdown report with recall, phantom count, and gate accuracy per category. Score failures are advisory. A nonzero exit means the run itself was incomplete. Use --runs 3 to expose variance, and --concurrency or --timeout-ms to tune the pool. Paste the report into the PR and compare it with the previous report on the same harness and model. Artifacts land under .ai/kenkeep/.state/prompt-eval/ and must not be committed.
Every frontmatter and JSON state file in the system carries a schema_version. Nodes, ENTRY.md and GRAPH.md are at 3, conflict files at 2, and session logs, config.yaml and the other JSON state files at 1. The policy is strict: any breaking change to the on-disk shape gets a clean break - no compatibility shims and no legacy code paths in the readers. The reader rejects the old shape, and users on it run the migration for that bump (/kk-migrate). Re-running init does not migrate existing data.
Concretely:
- Bump
schema_versionwhen: removing a field; renaming a field; changing the semantics of a field; making a previously-optional field required. - Do not bump when: adding an optional field; adding a new enum case; relaxing a constraint.
When you bump, the reader rejects older files with a clear error directing the user to run the kk-migrate skill in their agent session (migration requires an interactive session; re-running init does not migrate existing nodes).
Each src/templates-source/prompts/*.md and each skill's SKILL.md.hbs carries a top-of-file Version: N comment. Bump the version when you change behavior, once per release. Prompt version is independent of the npm package version, but a prompt change must be noted so users know to inspect the diff. CHANGELOG.md is generated by semantic-release, so do not edit its released sections. Put the prompt and behavior summary in the PR description.
Releases are automated via semantic-release. Conventional commit messages (feat:, fix:, refactor:, docs:, test:, chore:) determine the next version and changelog entry. Merging to main triggers the release pipeline; no manual tagging or npm publish is needed.
The docs site is Jekyll under docs/. To preview locally:
cd docs
bundle install
bundle exec jekyll serveCI deploys on push to main.
A "harness" is one of the assistant CLIs we drive (Claude Code, Codex CLI, Cursor, OpenCode, GitHub Copilot CLI, ...). Each harness lives under src/harnesses/<id>/ and ships as a HarnessAdapter implementation. Codex and Cursor are the shell-hook reference adapters (.codex/hooks.json and .cursor/hooks.json); OpenCode uses a plugin shim; Copilot uses a per-event JSON hook document (.github/hooks/kk.json, repo-level). To wire up a new one (call it <id>):
-
Implement
HarnessAdapter. The interface lives insrc/harnesses/types.ts. Provideid,hooks,paths,install,upgrade,parseTranscript,renderTranscript,runHeadless,buildHarnessOpts,doctorChecks,listMemoryFiles,launchModelArgs(native model flags for the launchers), and (optionally)detectFromEnv. Define<id>Pathsonce ininstall.ts;paths,initanddoctorall read it. BuildrunHeadlessonsrc/lib/headless-runner.ts, so the argv/stdin transport and the recursion guard stay shared. -
Declare your event vocabulary.
HookEventis opaquestring; each adapter declares the event names its host runtime actually emits (Claude usesStop/SessionEnd/...; Codex reuses Claude names; OpenCode usessession.idle/session.created). Pick whatever names the runtime exposes natively; do not translate to a global enum. -
Choose
hooksDirorpluginsDirinpaths(root). Adapters whose host runtime fires per-event shell commands usehooksDir. Adapters whose host runtime expects a long-lived plugin module subscribed to an event bus (OpenCode) usepluginsDirinstead; the build pipeline auto-detects a siblingsrc/harnesses/<id>/plugins/directory and emits its TS sources totemplates/<id>/plugins/, plus renames the hook output tokk-hooks/(so.opencode/kk-hooks/does not collide with the runtime-reserved.opencode/hooks/). When your adapter has no plugin shim but still needs its hook scripts kept apart from a<dir>/hooks/that holds a config artifact (Copilot storeskk.jsonthere), drop an emptysrc/harnesses/<id>/.kk-hooks-outputmarker file: the build honors it the same way, emitting scripts totemplates/<id>/kk-hooks/. -
Declare
payloadonHookSpecentries when the host hook-config schema needs per-entry metadata.HookSpec.payloadis an opaqueRecord<string, unknown>consumed only by your own adapter'shooks-configwriter; shared code never reads it. Copilot uses it to carry the per-event{ type, timeoutSec, env }knobs its JSON hook format requires, and renders them into the native shape inhooks-config.ts. Adapters whose host needs no per-entry metadata leave it unset. -
Register the adapter. Add it to the central registry in
src/harnesses/registry.tsso--harness <id>and the env detector pick it up. The registry is the only list of harness ids; skills carry no allowlist of their own. -
Add hook scripts. Place compiled-source hook scripts under
src/harnesses/<id>/hooks/(one.mjsper hook). The build pipeline inscripts/build-templates.mjsauto-discovers them and emits them into the bundledtemplates/<id>/hooks/tree (orkk-hooks/, see step 3). -
Add templates. Place static template assets (settings stubs, harness-specific config) under
src/templates-source/<id>/. Skills are not per-harness: the sharedsrc/templates-source/skills/tree installs identical SKILL.md bytes into every configured harness's native skills dir. -
Add doctor checks. Implement harness-specific health probes (CLI on PATH, settings file validity, hook registration intact) in
src/harnesses/<id>/doctor.tsand surface them via the adapter'sdoctorChecks(paths)method. -
Add a
ModelChoiceSchemadiscriminator option. The discriminated union insrc/lib/schemas.tskeys per-call model selection on theharnessfield. Add a new schema variant ({ harness: '<id>', ... }) soproposalModel,curatorModel, andbootstrapModelaccept your harness inconfig.yaml. -
Wire up env detection (if your runtime exports an in-session env var). Add an env detector to the adapter's
detectFromEnv;resolveActiveHarnessconsults every registered adapter in order, and no skill or script keeps a parallel detector list. Copilot exports no in-session env var, so it omitsdetectFromEnvand relies on--harness/--hint/cliDefaultHarness. -
Write tests. Add the adapter to the parametrized case tables under
tests/harnesses/(transcript,headless,headless-transport,hooks-config,list-memory-files) rather than creating a per-adapter directory. Cover at minimum: transcript parsing, hook registration round-trip, doctor checks (tests/doctor.test.tsruns the live ones), headless-run option mapping, and the stdin transport for prompts over 64 KiB.
Adapters never reach into each other's directories. Anything shared (paths under .ai/kenkeep/, the curator pipeline, the node schema, the SKILL.md tree) lives in the harness-neutral modules under src/lib/ or src/commands/, or under src/templates-source/skills/.
- One logical change per PR. Branch from
main. - Include doc updates alongside the code change in the same PR.
- Run
npm test,npm run typecheck,npm run typecheck:tests, andnpm run lintbefore pushing. - Conventional commit format on commit messages and the PR title.