Commands
npm run typecheck # tsc --noEmit
npm test # build, then 1,242 tests — no real LLM calls
npm run watch # rebuild on save
npm run build # tsc -p tsconfig.json
npm run schema # regenerate config.schema.json from zod
npm run smoke # one real LLM call (needs a live provider)
npm run release:check # npm pack --dry-run
node src/cli.ts doctor # headless commands run from source, no build
npm test builds dist/ first: the TUI tests render real Ink components, and
Node cannot load .tsx directly.
Two rules that bite
!!! warning “The TUI only runs from dist/”
Node runs .ts directly but not .tsx — JSX is not stripped. Headless
commands (run, serve, doctor, …) run from source with
node src/cli.ts; the TUI needs npm run build first.
!!! warning “config.schema.json is generated”
It comes from src/core/schema.ts via zod. Do not edit it by hand — run
npm run schema after changing the schema, or the two silently disagree.
Layout
src/
core/ the agent loop, permissions, storage, config, rpc
core/tool/ every built-in tool, one file each
core/storage/ SQLite plus the blob directory
core/delegate/ external-CLI adapters
server/ HTTP + SSE, and the embedded web client
tui/ Ink components
scripts/ schema generation, smoke test
test/ node:test suites
docs/ design specs and gap analyses
editor/nvim/ a Neovim plugin
The boundaries are deliberate: the package ships as one npm module, but core
knows nothing about server, and server knows nothing about tui. That is
what makes titah run able to call the core in process while titah goes over
HTTP, with no second implementation.
Conventions
Code comments in this repository are written in Indonesian; the user-facing interface and all documentation are in English. That split is intentional and consistent — do not mix them.
What the tests cover
Thorough unit tests concentrate on the parts that fail silently:
edit— string matching, the non-unique case, and that a miss writes nothingbash— timeout and cancel, including the process group- the permission engine — the full check order, including auto-deny with no client attached
- delegation adapters against a stubbed external CLI
That last one matters most: tests that burn tokens never get run, so the adapters are tested against a stub that speaks the same JSON the real CLIs do.
Three to five end-to-end scenarios against a small fixture repo are run manually before a release rather than in CI. Jumping to a full eval before the core is stable means debugging the harness instead of the product.
The smoke test
node scripts/smoke.ts
One real LLM call against a live provider. It is deliberately not part of npm test — a suite that needs credentials and network is a suite people stop
running.
Adding a tool
- Implement it in
src/core/tool/, and register it in that directory’sindex.ts—allTools()is the single list. - Put it on a permission axis — reuse an existing one only if it protects
the same thing.
networkexists because confidentiality is not covered bybash;mcpexists because unclassifiable third-party code is not covered by either. - Decide what happens on failure. A tool that half-succeeds is worse than one
that refuses — see
editandpatch. - Add tests for the silent-failure paths, not the happy one.
- Update
README.md’s tool table and this documentation.
Adding an external agent adapter
Usually you do not need to: the registry is config-driven, and
externalAgent.<name> with command, args and format covers most CLIs
without touching code. See Delegation.
Write an adapter only when the CLI needs a different protocol rather than
different arguments. The internal interface is shaped like ACP — prompt,
sessionUpdate, requestPermission, cancel — precisely so a future ACP
transport is one new adapter rather than a rebuild.
Releasing
npm run release:check # npm pack --dry-run
prepublishOnly runs typecheck, schema, build and test in that order, so
a release cannot ship with a schema that has drifted from the zod source.
Reporting a bug
Include the output of:
titah doctor --probe
It reports environment, config locations, the merged config (redacted), which
credentials resolved, which external agents were detected, skills per namespace,
and every model missing a contextWindow — which is most of what any bug report
needs.
Issues go to the tracker on GitHub.