Kotoba

Where to start

What Kotoba isWho she is and where she runs, in one page
InstallingOne package, two commands
First runA key, a model, her first words
The approval gateHow she asks before she acts
The two voice modesLocal voice, or the agent tunnel
The soul fileChange who she is
RoadmapWhat grows next, and what was cut on purpose

Or a section

↑↓ move openesc close124 pages
All pages

Contributing and reference3 of 12

House rules

About 5 minutes to read

On this page

These are the conventions the repository actually holds itself to. Some are enforced by a test; those say which one. The rest are enforced at review, and this page says so rather than pretending otherwise.

Everything is written in English

Code, comments, commit messages, test names, docstrings. The product speaks whatever language its user speaks; the repository speaks one.

A comment says WHY, and stops

The code already says what. A comment earns its place by explaining a decision the code cannot: the trap that was hit, the alternative that was rejected, the measurement that settled it.

  • Ten lines is the ceiling for any one comment or docstring.
  • A long docstring is the same violation wearing a different hat. Moving a wall of # into """ does not shorten it.
  • A comment that restates the line under it gets deleted.

Not enforced by a test. There is no comment-length check in either suite; the pull request template carries the checkbox and a reviewer reads the diff.

A fix arrives with a test that was seen failing

The pull request template puts it plainly: a test that fails without the change and passes with it is the strongest form of this; "it should work" is the weakest.

So write the test first, run it, and watch it go red. A green test written after a green fix proves that the two agree, not that either is right. This repository has been bitten by that shape more than once — guards that had never fired, and therefore measured nothing.

Two habits that follow from it:

  • Break what a guard guards, and confirm the guard goes red. If you cannot make it fail, it is not an instrument.
  • Run the command and read the output. "Should work" is not an acceptable claim in a pull request description; the template asks for the command you ran and what it said.

Nothing in the repository names a person or a machine

This one is enforced, by api/tests/test_the_repository_belongs_to_nobody.py — seven tests over the whole shipping tree: api/src, api/tests, lib, components, app, scripts, docs, soul, assets, tests, public/worklets, plus every top-level config and documentation file by name.

It refuses:

ShapeExample of what fails
A real home directory/home/<a real name>, /Users/<a real name>, C:\Users\<a real name>
A personal directory as a tilde path~/<a folder only one machine has>
A mailbox outside the reserved documentation domainanything not @example.com
A decision credited to a named individualSomeone chose …, Someone's call
A real Discord account, server or role ida snowflake whose timestamp decodes to a real moment
The author's name outside the four places it belongsanywhere but pyproject.toml, LICENSE, package.json, README.md
The name of the tooling that wrote the codeany occurrence, anywhere

Two details worth copying if you ever write a guard like it:

  • It is an allowlist of what is generic, not a blocklist of names to avoid. A blocklist would have to spell out the name it removes, and would go stale the moment anybody else worked on the tree.
  • The credit check is structural: a capitalised word followed by a verb of authorship. It catches a name without ever writing one down.

The rule underneath is the fact survives, the person does not. A measurement stays. The person who asked for it goes, because a reader who cannot ask that person is left with an argument from authority they have no way to check.

Placeholders exist for when a literal path is unavoidable — a path-elision test needs a real absolute path to shorten. Use one already in PLACEHOLDER_HOMES; adding to that list is a deliberate act, and adding a real home directory to it cannot be.

No secrets in a commit

Never a key, a token, or a .env with real values. The templates to copy are api/.env.example and .env.local.example. .gitignore already refuses .env, .env.* and *.env, with the two example files re-admitted by name.

No emoji in the interface

Enforced, by tests/no-emoji-in-the-interface.test.mjs. It reads every .ts/.tsx under components/, app/ and lib/ and fails on any pictographic codepoint. The reason is practical: an emoji is the one glyph the design cannot style, size or recolour, and it renders as somebody else's artwork on every platform. The interface's own marks — ✓ ✕ ⚠ ✦ ♥ ★ — are typography and stay.

Registration is explicit; an import is not a registration

A tool module is offered to the model because it is named in a list, not because it exists: tools/registry.py's discover() for a built-in, tools/action/__init__.py's ACTION_TOOLS for an action tool. A module left off the list is silently offered to nobody — nothing raises, she simply cannot do the thing.

Enforced, by api/tests/test_no_tool_module_is_left_unregistered.py, which asks the file system rather than the import graph: every .py in either package must reach the live registry.

A tool declares its voice patterns

ANNOUNCE, HEARTBEAT, COMPLETE, FAIL, beside SCHEMA and execute. She narrates before a tool runs, again while it is still going, and confirms when it lands. A tool without them goes quiet mid-turn.

There is exactly one module in the tree with no ANNOUNCE: tools/builtin/web_search.py, and its silence is deliberate — the provider runs that search server-side, so the narration branch it would use is never reached. Counted at the time of writing; it is the only one.

The fourteen emotions live in more than one file

Measured at the time of writing: api/src/kotoba/core/emotions.py (VALID_EMOTIONS), api/src/kotoba/soul/prompt.py (EMOTIONS) and lib/expressions.ts (EMOTIONS) all hold the same fourteen names.

api/tests/test_the_three_emotion_lists_agree.py compares all three, in both directions, so a name added to one and not the others goes red. If you add an emotion, change all three — and then the terminal's kaomoji table, both trees of face sprites, and the fallback list in lib/avatar-config.ts, which that test does not cover. Which expression a model plays for an emotion is a separate, per-model question.

No Live2D model is ever committed

public/models/ is gitignored and must stay that way. The good models — including Live2D's own free samples — forbid redistribution, so a model pushed here would republish somebody else's work under this repository's licence. Models live on the user's machine, in ~/.kotoba/models/.

scripts/build_web.py refuses to build at all if it finds a .moc3 or a .model3.json anywhere under public/, symlinked directories included.

Formatting

.editorconfig decides it, so editors do not fight the diff: UTF-8, LF, a trailing newline, trimmed trailing whitespace, 110 columns, 2-space indent everywhere except Python, which is 4.