ADR-0025: Typecheck orchestrates per-package; root tsc is a marker
- Status: Accepted
- Date: 2026-05-27
- Deciders: @karasu
- Supersedes: —
- Superseded by: —
Context
Until now, the repo had a single root tsconfig.json that every workspace package extended via "extends": "../../tsconfig.json", and the root config also acted as a default typecheck entrypoint (bunx tsc --noEmit from the repo root). That arrangement was broken in two unrelated ways:
Library mismatch. The root config declares
"lib": ["ESNext"]and"types": ["bun"]because most packages are server-side Bun code.packages/uioverrides those to add["ESNext", "DOM", "DOM.Iterable"]and["bun", "vite/client"]. Whentsc --noEmitruns at the root, it discoverspackages/ui/**/*.tsxvia default include globs and types them with the root's no-DOM lib, producing dozens of false-positiveDOM is not definederrors. The narrative the AGENTS.md gotchas section gave for "root typecheck is broken" was this.Real per-package drift. Independent of (1), several packages have accumulated real type errors that only surface when their own
typecheckscript runs (daemon,harness,llm,ui,utils,webhook-notify). STATUS.md item 71 ("per-package typecheck passes clean") was stale; per-package was no longer green either.
The fix the docs gestured at — "needs project references or composite builds" — is structurally incompatible with how this repo wires cross-package imports. Packages consume each other through TS paths that point at sibling source files (@kaged/dsl → ../dsl/src/index.ts); Bun resolves those at runtime, no build step exists or is wanted. TS project references require referenced projects to set composite: true, which forces declaration: true, which forces emitting .d.ts artifacts that nothing in this repo consumes. With TS 5.6+, the composite + noEmit escape hatches narrowed further, and the declaration-nameability rules (TS4023 / TS4058 / TS9006) would surface a fresh wave of errors orthogonal to the original problem. Going down that path trades a single broken command for an invasive rewrite of every workspace package's tsconfig in service of nothing the runtime needs.
This ADR locks in the orchestration choice and stops the doc drift.
Decision
kaged typechecks per-package via
bun --filter '*' typecheck. The roottsconfig.jsonis intentionally a marker file that does not typecheck anything itself. Shared compiler options live intsconfig.base.json, which every workspace package and system plugin extends.
Specifics
tsconfig.base.jsonat the repo root holds the sharedcompilerOptionsblock (strict flags, module mode, target, etc.). It has noincludeand nofiles. Nothing extendstsconfig.jsonfor compiler options.tsconfig.jsonat the repo root is a marker. It extendstsconfig.base.jsonand pointsincludeat a singletypecheck-orchestration-marker.d.tsfile containing onlyexport {}and a comment explaining the orchestration. The marker file exists to satisfy TS's "no inputs" check (TS18003) without including any real source code. Runningbunx tsc --noEmitat the root types only this marker and exits clean — it does nothing useful, by design.bun run typecheckis the canonical command. It expands tobun --filter '*' typecheck, which runs each package's owntypecheckscript (tsc --noEmitinside the package directory, against the package's own tsconfig). Each package's tsconfig declares its ownlib,types,paths, andincludeset, sopackages/uigets DOM libs and the daemon does not.Each package's
tsconfig.jsonextends../../tsconfig.base.json(system plugins extend../../../tsconfig.base.json). No package extends the roottsconfig.json.Cross-package imports keep using TS
pathsto source files. This ADR does not introduce project references,composite: true, declaration emission, or any build step between packages. Bun resolves.tsdirectly at runtime; TypeScript resolves the same via path mappings.The root
tsconfig.jsonstays in place even though it does nothing — editors, language servers, andlsp-bridge-runtime.ts's root markers use its presence to identify the project root.
Consequences
What this commits us to
- Every new workspace package and every new system plugin must extend
tsconfig.base.json, nottsconfig.json. - If a shared compiler-option flag needs to change, it changes in
tsconfig.base.jsononce. - The canonical typecheck command is documented in this ADR, in the root
tsconfig.jsonheader comment, inAGENTS.md, and inSTATUS.md. All four must agree. - Per-package real type errors are now the only typecheck story; they cannot hide behind "but the root typecheck is broken anyway." STATUS.md must report per-package typecheck status honestly.
What this forecloses
- TypeScript project references / composite builds. Adopting them later would require revisiting cross-package import strategy (paths vs build outputs), introducing per-package declaration emission, and resolving the resulting TS4023/TS4058/TS9006 declaration-nameability errors. Not currently worth it; no consumer needs
.d.tsartifacts. - A single root command that typechecks the whole repo via
tsc -b.bun run typecheckis the equivalent and is shorter to type.
What becomes easier
- Adding a new package: copy any existing
packages/*/tsconfig.json, change the package name, done. No root tsconfig edits needed. - Per-package environment differences (UI's DOM libs, future packages targeting other environments) live in the package that needs them.
- Diagnosing typecheck failures: the failing package is named in the
bun --filteroutput prefix.
What becomes harder
- "Just run
tscat the root" no longer works. This is documented in three places (root tsconfig header, AGENTS.md gotchas, the marker.d.tsfile). If an operator runs it anyway, they getEXIT: 0with no output, which is non-obvious but at least non-destructive. - Operations that genuinely want a single TS process across the whole monorepo (a hypothetical incremental build cache, a single editor TS server instance with all packages loaded) lose that path. Not currently a use case.
Alternatives considered
Alternative A — TypeScript project references with composite: true
What it was: convert root tsconfig.json to a solution-style file with references listing each package; add composite: true + emitDeclarationOnly: true + outDir: node_modules/.tmp to all 18 package tsconfigs.
Why it was tempting: it is the documented TypeScript answer to monorepo orchestration and produces a working tsc -b at the root.
Why we didn't pick it: composite mode forces declaration emission, which forces declaration nameability rules, which would surface a fresh wave of TS4023/TS4058/TS9006 errors across packages that import from node_modules dependencies (Mastra, Langfuse SDK, Zod, etc.) where re-exported types can't always be named. We would also be generating .d.ts artifacts that nothing consumes; Bun reads .ts directly. The cost is real, the benefit is a command we don't need.
Alternative B — Add DOM libs to root tsconfig
What it was: just add "DOM" and "DOM.Iterable" to the root lib array so packages/ui files stop erroring at the root.
Why it was tempting: smallest possible change.
Why we didn't pick it: AGENTS.md already explicitly forbids it ("do not 'fix' by adding DOM to root; per-package is the design"). It also leaks browser globals into server-side packages, defeating the strictness that makes the server packages catch accidental DOM-API usage.
Alternative C — Delete root tsconfig.json entirely
What it was: no root tsconfig at all; bunx tsc from root finds nothing and exits with TS5057 ("Cannot find tsconfig").
Why it was tempting: minimum code, maximum honesty.
Why we didn't pick it: lsp-bridge-runtime.ts and editors use tsconfig.json at the repo root as a project-root marker. Removing it would degrade editor integration for an aesthetic gain.
References
- TypeScript issue #40431 — project references + noEmit
- TypeScript issue #59951 — TS 5.6 composite + noEmit tightening
- TypeScript handbook — Project References
- ADR-0003: Doc-first, then test-driven — this ADR follows the doc-first rule for an orchestration change
- ADR-0004: Runtime is Bun + TypeScript — direct
.tsresolution at runtime is why we don't need.d.tsemission between packages