KnightCode's system prompt and tool definitions sit at a measured floor of
roughly 1,100 tokens. Every byte added to the core is paid for on every
request, by every user, forever. Before adding to packages/cli/src/core or to a
built-in tool description, ask whether it belongs in an extension, a skill, or a
prompt template instead. See CONTRIBUTING.md.
Bun workspaces: packages/* and apps/*. Source runs directly under Bun; there
is no build step for development.
| Package | What it is |
|---|---|
packages/cli |
@knightcodeai/cli — the knightcode command: CLI, TUI mode, print mode, RPC mode, sessions, extensions, skills. The only published package. npm gets an esbuild bundle that runs on Node (bun run build:cli, scripts/build-bundle.ts); Bun-compiled standalone binaries (bun run build:binary, scripts/build.ts) are GitHub release downloads only |
packages/ai |
Multi-provider LLM layer: providers, API adapters, OAuth, model catalog |
packages/agent |
Agent loop, proxy stream, and their types |
packages/durable |
Durable conversation, task and document runtime (Pico): record contracts and storage |
packages/tui |
Terminal UI library with differential rendering |
packages/app-server-protocol |
Schemas and types of the app server protocol; imports nothing from ai or agent |
packages/telemetry · evals |
Telemetry contracts, eval harness |
apps/web |
The website. Separate toolchain; not covered by the root type check |
apps/desktop |
The Electron desktop app (Forge + Vite + React). Talks to knightcode app-server; own tsconfig.json and Vitest run, not covered by the root type check |
Everything except packages/cli* and apps/desktop is private and consumed
through the workspace.
The root tsconfig.json maps every @knightcode/* and @knightcodeai/cli
specifier to package source, so imports resolve without a build.
User-facing documentation lives in packages/cli/docs/. Keep it in sync when you
change flags, settings, keybindings, providers, or the session format.
- Keep answers short and concise
- No emojis in commits, issues, PR comments, or code
- No fluff or cheerful filler text (e.g., "Thanks @user" not "Thanks so much @user!")
- Technical prose only, be direct
- Use concise, clear, simple language. Define unavoidable jargon before using it.
- Explain non-trivial designs and problems as: problem, concrete example or short trace, then solution. State why the solution is necessary and distinguish it from optional complexity.
- Prefer concrete behavior and small illustrations over abstract summaries, dense terminology, or unexplained lists of changes.
- When the user asks a question, answer it first before making edits or running implementation commands.
- When responding to user feedback or an analysis, explicitly say whether you agree or disagree before saying what you changed.
- Read files in full before wide-ranging changes, before editing files you have not fully inspected, and when asked to investigate or audit. Do not rely on search snippets for broad changes.
- Formatting is Prettier, configured in
.prettierrc: tabs, 120 columns, LF.bun run formatformats.ts/.tsx; check without writing viabun x prettier --check "**/*.ts". Prose is not formatted — Markdown is hand-wrapped, keep it that way. .prettierignorecovers what must not be touched: generated model catalogs, test fixtures (several are deliberately malformed), Changesets-owned changelogs, andapps/(own config and toolchain). Don't format those by hand either.- No
anyunless absolutely necessary. - Inline single-line helpers that have only one call site.
- Check
node_modulesfor external API types; don't guess. - No inline imports (
await import(),import("pkg").Type, dynamic type imports). Top-level imports only. - In
packages/cli, resolve package assets through helpers insrc/config.ts. Do not use__dirnamedirectly; the helpers account for source checkouts, npm installations, and standalone binaries. - Never remove or downgrade code to fix type errors from outdated deps; upgrade the dep instead.
- The root config sets
erasableSyntaxOnlyand coverspackages/*/srcandpackages/*/test: no parameter properties,enum,namespace/module,import =,export =, or other constructs needing JS emit. Use explicit fields with constructor assignments. - Always ask before removing functionality or code that appears intentional.
- Do not preserve backward compatibility unless the user asks for it.
- Never hardcode key checks (e.g.
matchesKey(keyData, "ctrl+x")). Add the binding toKEYBINDINGSinpackages/cli/src/core/keybindings.tsorTUI_KEYBINDINGSinpackages/tui/src/keybindings.tsso it stays configurable. - Never edit
packages/ai/src/models.generated.tsor thesrc/providers/*.models.tswrappers directly; change the provider definitions inpackages/ai/catalog/(see its README) and regenerate withbun run generate:models. Including the resulting generated diff is always OK, even when regeneration pulls in unrelated upstream model metadata. packages/ai/src/providers/data/is generated from the live catalog sources and is not committed, exceptagentrouter.json: AgentRouter answers a WAF interstitial to datacenter IPs, so no runner can fetch it and the generator falls back to that committed file. A fresh checkout has no catalog otherwise, so nothing type-checks, tests or builds until you runbun run hydrate:model-data. Because the generated types pin what tests may name, a provider dropping a model turns the type check red; fix the test rather than the catalog.- Code must work on Windows as well as POSIX. Path joins, spawned shells, and line endings are the usual breakages;
.gitattributesnormalizes to LF except for.bat/.cmd/.ps1.
Binding for packages/chord/src/next/ and its tests, when writing and when reviewing. The model is Durable's source (packages/durable).
- Files: one concept per file, named after it; no one-helper files, no grab-bag files. First line: a comment saying what the file is for. Order: imports, exported types, then the public entry point first and its helpers below it in the order they are used; constants right above their first user. A blank line between top-level declarations. Split files by concept, never by size: one concept may take a long file (up to about 1,000 lines is fine); a second concept in a file is the reason to split.
- Functions: every export has a one-line doc comment saying what it does or guarantees. Inline comments explain a non-obvious why (cost, race, decided rule), never what the next line does. Short functions with one job (about 40 lines needs a reason; one
switchover a tagged union is fine). No boolean flag parameters that switch behaviour: two functions or a tagged value. Never write the same lines twice. Context last: required args, thencontext/ctx(always required, as in Durable; optional only on error-reporting paths), then optional args. - Types and checks: tagged unions for every state and result. Casts only where TypeScript cannot express a decided invariant, each with a short why. Every remaining runtime check names the trust boundary it serves (wire input, schema-checked params and arguments, declaration policy the types cannot express). Same-host values are plain data, immutable by convention: never copied, frozen or walked to enforce it; freeze only Chord-owned metadata when a decision says so. No defensive code against broken adapters, callers or our own modules.
- Cost and memory: no hot-path work a decision did not ask for (walks, copies, compilations, per-call allocations). The function doing counted work counts it through its context; call sites never count for others. Caches are
WeakMaps keyed by their owner, each with a GC test; no module-level strong caches. - Names and API: the design's vocabulary (
params,binding,principal key), no invented synonyms; decidedChordErrorcodes and reasons. Export only what a later layer or app needs. No dev or test modes, no unrequested options. - Tests: one test file per source file, one
describeper mechanism, the layer spec's test list case by case; cost guards through the in-memory telemetry adapter; GC tests for every cache and binding. Every slice also adds one or more runnable examples inpackages/chord/test/next/examples/NN-topic.ts, one topic each, numbered across slices, written as references for Chord's users (Durable'stest/examples/format: header with what it shows and how to run it, commented steps, printed results). - Slices: Chord v2 is built in vertical slices (
packages/chord/docs/next/slices.md). Implementers and reviewers read the slice spec, the feature map inslices.mdand the whole ofpackages/chord/docs/next/design.mdin full, and check the work against all three; slice specs cite design sections for orientation, not as a reading limit. - Deferred work: anything left to a later slice goes into the section "Carry-over" of
packages/chord/docs/next/slices.mdunder that slice, in the same change; a slice's spec and build include every entry of its section. - Before reporting: re-read every changed file top to bottom as a stranger would; each file must be understandable without jumping around. Then
bun run check-typesand the layer's tests.
Run from the repo root unless stated otherwise.
- After code changes (not docs):
bun run check-types(full output, no tail). Fix every error before committing. It does not run tests. - Never run
bun run build:cli,bun run build:binary, or the fullbun run testunless the user asks. - Tests are per-package. Run the specific test you touched from the package root:
- Vitest packages (
ai,agent,cli,evals):bun x vitest --run test/specific.test.ts packages/tui(node:test):node --test test/specific.test.ts
- Vitest packages (
- Whole-package runs work (
cd packages/cli && bun run test), but on Windows full-suite runs flake on tests that spawn shells. Re-run a failure in isolation before treating it as a regression. - Some
packages/aitests hit real provider endpoints. They self-skip when the credential is absent (describe.skipIf(!oauthToken),it.skipIf(!process.env.X_API_KEY)) — but a populated root.envactivates them, so a fullpackages/airun spends real tokens and can fail on provider rate limits (HTTP 429) rather than on your change. They are not confined to*-e2e.test.ts;stream.test.ts,tokens.test.tsand others carry live suites too. Check the failure message before treating one as a regression, and never add a live-endpoint test without that guard. - If you create or modify a test file, run it and iterate on test or implementation until it passes.
- For
packages/cli/test/suite/, usetest/suite/harness.tswith the faux provider (packages/ai/src/providers/faux.ts). No real provider APIs, keys, or paid tokens. - When adding a regression test for a GitHub issue, put the issue number in a comment next to the test.
- For ad-hoc scripts, write them to a temp file, run, edit if needed, remove when done. Don't embed multi-line scripts in
bashcommands. - Never commit unless the user asks.
packages/ai/src/providers/ holds one <name>.ts per provider, a
<name>.models.ts catalog module, and a generated data/<name>.json metadata file, which
is hydrated locally rather than committed.
- Add the provider module and register it in
providers/all.ts. - Add its catalog definition under
packages/ai/catalog/providers/withdefineProvider()and register it incatalog/providers/index.ts(readpackages/ai/catalog/README.mdfirst; a models.dev provider usually needs onlymodelsDevModels()and per-APIcompat). - Regenerate the catalog with
bun run generate:models(orbun run hydrate:model-datafor metadata only, which is also how a fresh checkout gets one). Verify withbun run check:model-data. - Add tests under
packages/ai/test/named<provider>-*.test.ts. Required at minimum: request construction (headers, base URL, auth), streaming/SSE parsing, and model catalog presence. Follow the closest existing provider's tests. - Anything needing a live key goes in a
*-e2e.test.tsgated on that key's env var.
User-facing notes belong in packages/cli/docs/providers.md, or
custom-provider.md for endpoint-configured providers.
- Treat dependency and
bun.lockchanges as reviewed code. Direct external deps stay pinned to exact versions (.npmrcsetssave-exact=true). Newly resolved versions must be two days old:bunfig.tomlsetsminimumReleaseAge, the setting bun enforces, and.npmrckeepsmin-release-age=2for npm, which bun does not read. The gate only filters resolution; bun installs whatbun.lockalready records without rechecking it, frozen installs included, so reviewing the lockfile diff stays the control for those entries. - When updating
undici, you MUST read its changelog/release notes for the target version and evaluate whether any changes may affect functionality before applying the update. - Install with
bun install; CI-style withbun install --frozen-lockfile. bunfig.tomlpinslinker = "hoisted"on purpose: the isolated layout givespackages/aiandpackages/cliseparate copies of a dependency, which breaksvi.mock()across package boundaries. Do not change it.- New deps with lifecycle scripts require review; never add one silently.
- Pre-commit blocks
bun.lockcommits that change external packages unlessKNIGHTCODE_ALLOW_LOCKFILE_CHANGE=1. Don't bypass unless the user wants the lockfile change committed.
Multiple sessions may be running in this cwd at the same time, each modifying different files. Git operations that touch unstaged, staged, or untracked files outside your own changes will stomp on other sessions' work. Follow these rules:
Committing:
- Only commit files YOU changed in THIS session.
- Stage explicit paths (
git add <path1> <path2>); nevergit add -A/git add .. - Before committing, run
git statusand verify you are only staging your files. packages/ai/src/models.generated.tsmay always be included alongside your files.- Message format:
{feat,fix,docs,refactor,chore}[(<scope>)]: <message>, scope being the package short name (ai,agent,cli,tui,server,client,protocol,telemetry,evals,web). Informative and concise. - Never add
Co-Authored-Byor other AI-attribution trailers to commit messages.
Never run (destroys other agents' work or bypasses checks):
git reset --hard,git checkout .,git clean -fd,git stash,git add -A,git add .,git commit --no-verify.
If rebase conflicts occur:
- Resolve conflicts only in files you modified.
- If a conflict is in a file you did not modify, abort and ask the user.
- Never force push.
See CONTRIBUTING.md for the contributor gate (auto-close workflows,
lgtm/lgtmi, quality bar).
When reviewing PRs:
- Do not run
gh pr checkout,git switch, or otherwise move the worktree to the PR branch unless the user explicitly asks. - Use
gh pr view,gh pr diff,gh api, and localgit show/git diffagainst fetched refs to inspect PR metadata, commits, and patches without changing branches. - If you need PR file contents, fetch/read them into temporary files or use
git show <ref>:<path>without switching branches.
When posting issue/PR comments:
- Write the comment to a temp file and post with
gh issue/pr comment --body-file(never multi-line markdown via--body). - Keep comments concise, technical, in the user's tone.
- End every AI-posted comment with the AI-generated disclaimer line specified by the originating prompt.
Triage labels are managed by .github/workflows/issue-triage-labels.yml:
untriaged, to-discuss, inprogress, last-read, no-action. Do not set
them by hand unless the user asks.
When closing issues via commit:
- Include
fixes #<number>orcloses #<number>in the message so merging auto-closes the issue. For multiple issues, repeat the keyword per issue (closes #1, closes #2); a shared keyword (closes #1, #2) only closes the first.
bun run start runs the TUI from source with .env loaded. For scripted
interaction, drive it in a controlled terminal (POSIX; from the repo root):
tmux new-session -d -s kc-test -x 80 -y 24
tmux send-keys -t kc-test "bun run start" Enter
sleep 3 && tmux capture-pane -t kc-test -p # capture after startup
tmux send-keys -t kc-test "your prompt here" Enter
tmux send-keys -t kc-test Escape # special keys (also C-o for ctrl+o, etc.)
tmux kill-session -t kc-testThere is no tmux on Windows. Either run bun run start and let the user drive
it, or exercise the same paths headlessly with bun run start --print "...".
Changelogs are generated by Changesets. Never hand-edit
packages/*/CHANGELOG.md or apps/desktop/CHANGELOG.md.
For a user-visible change, add a changeset and commit it with your work:
bun run changeset [category] [bump] [cli|desktop] # defaults: fixed patch cliThat scaffolds .changeset/<branch>.md for @knightcodeai/cli (or
@knightcode/desktop with desktop) with the category word already in place,
and you finish the sentence:
Fixed Windows shell aborts crashing KnightCode when
taskkill.exeis missing.
The first word of the summary is the category — Added, Changed,
Deprecated, Removed, Fixed or Security — and it decides which Keep a
Changelog heading the entry lands under. It cannot live in the frontmatter:
@changesets/parse reads every frontmatter key as a package name. Do not copy
the imperative style of the entries already in packages/cli/CHANGELOG.md
("Add a flag", "Tidy the transcript") — they predate the convention and would
now be rejected.
CI runs bun run scripts/changelog-sections.ts (the "Check changesets" step),
which fails on a summary that does not start with a category followed by a
sentence. Run it locally before pushing; it also self-checks the regrouping.
One changeset per change. An entry describing two things has to be filed under
a single heading, so two user-visible changes in one branch means two
changesets — a second bun run changeset on the same branch takes the next
free -2 suffix rather than refusing.
Changesets versions only the packages that are not private
(privatePackages.version is false in .changeset/config.json), and the CI
check refuses a changeset naming any other. There are two: @knightcodeai/cli,
the only package published to npm, and the desktop app, @knightcode/desktop,
which ships as installers. The desktop app is not private so that it gets a
version and a changelog; ci:publish only publishes the CLI, and its
prepublishOnly script refuses npm publish. A change to apps/desktop takes
a desktop changeset, and the Version Packages PR bumps
apps/desktop/package.json and writes apps/desktop/CHANGELOG.md; merging a
Version PR that carries only desktop changesets publishes nothing to npm. A
desktop change that also changes the app server takes one changeset for each.
Do not file desktop-only changes under @knightcodeai/cli: the CLI's changelog
is what the terminal and knightcode.dev show. Every other package is private,
never versioned, and takes no changeset; do not make one public to give it a
changeset.
Releases are automated by .github/workflows/publish.yml; there is no local
release command and nothing to publish by hand:
- A push to
mainwith changesets present opens or updates achore(release): version packagesPR (bun run ci:version, which also regroups the new changelog section under its category headings and drops the commit-hash prefixes). - Merging that PR leaves no changesets and a version npm does not have, which triggers the build matrix: each standalone binary is compiled on its own native OS and smoke-tested (
--version,--help).darwin-x64is cross-compiled and skips the smoke test — there is no free Intel runner. - The
publishjob re-runscheck-typesandtest, builds the Node bundle, runsbun run ci:publish(pack-test plusnpm publishwith provenance), pushes tags, and creates a GitHub Release with the per-platform binary archives,SHA256SUMS, and the installer lockfile.
The npm-publish environment holds NPM_TOKEN and can gate releases behind
required reviewers. If a publish fails partway, re-run the job — ci:publish
skips versions already on npm. Do not bump versions by hand to work around it.
The desktop app is released by hand with .github/workflows/desktop-release.yml
(Run workflow, optionally as a prerelease). It builds main at the version the
Version Packages PR gave apps/desktop, so merge that PR first; a version that
is already tagged, or has no ## <version> section in apps/desktop/CHANGELOG.md,
is refused. The draft release's notes are the changelog since the last
published desktop release, which the update feed shows as what's new. Publish
the draft with the release label None, never Latest: the repository's latest
release must stay the CLI's, since the CLI's download links point at
/releases/latest. A prerelease is skipped by the update feed and the
website's download buttons. Never bump apps/desktop/package.json by hand.
The model catalog is published separately by
.github/workflows/publish-model-catalog.yml from bun run generate:model-catalog; dry-run locally with bun run check:model-catalog.
If the user's instructions conflict with any rule in this document, ask for explicit confirmation before overriding. Only then execute their instructions.