This document defines non-negotiable modularity constraints for this codebase.
The goal is to enforce architecture continuously through tests/CI, instead of relying on occasional refactors.
- Boundaries are enforced by code, not convention.
- Violations fail CI.
- Exceptions are explicit, temporary, and tracked.
- Rule changes are architecture changes and must be reviewed as such.
- High-level intent:
ARCHITECTURE.md,REFACTOR.md - Enforcement:
test/unit/arch/modularity.test.ts - This file defines the policy layer and expected review/CI behavior.
Enforce one-way dependency flow and prohibit layer skipping.
src/core/**must not depend onsrc/ui/**orsrc/cli/**.src/core/**must not depend on concrete provider/tool implementations (only seam files — see B).src/core/**may reachsrc/tools/**only throughsrc/tools/host.ts(theToolHostseam).src/tools/**must not depend onsrc/core/**, except for the orchestration toolssrc/tools/delegate.tsandsrc/tools/invoke-skill.ts(façades over core capabilities; tracked exception, exit condition documented in the test).src/tools/**must not depend onsrc/ui/**(system never imports UI — UI consumes theAgentEventstream).src/security/**must stay primitive (no dependency on sibling top-level layers).src/providers/**must not depend onsrc/ui/**,src/tools/**,src/core/**,src/mcp/**,src/cli/**.src/providers/**must not importnode:fs. Allowlisted:copilot/auth.tsandgoogleaistudio/auth.ts(on-disk auth-token readers for external CLIs).src/ui/**must not depend onsrc/mcp/**.src/ui/**must not import concrete provider implementations (only seam files — see B).src/ui/**must not import concrete tool handler files (only seam files — see B).src/ui/headless.tsmust not depend onsrc/ui/tui/**.src/utils/**must not depend on any sibling top-level folder.src/index.tsis a thin bootstrap (≤30 non-trivial lines, no imports fromsrc/{core,providers,tools,mcp,security}/**). Cross-layer wiring lives insrc/cli/startup/main.tsand the phase helpers it composes.
These are enforced in test/unit/arch/layers.test.ts and should remain enforced.
Only stable seams are allowed across boundaries.
- Cross-layer imports must go through declared seam files. The canonical seam sets per boundary are defined in
test/unit/arch/boundary-surfaces.test.ts(and theexcept:arrays inlayers.test.ts). Changing a seam set is an architecture change; update both the test and this section together. src/tools/host.tsis the sole seam betweensrc/core/**andsrc/tools/**. It surfaces theToolHostinterface plus the type re-exports core needs (ToolHandler,ToolResult,ToolContext,ToolDefinition,TOOL_NAMES,ToolResolutionError).src/providers/openai/**is an internal adapter — only files withinsrc/providers/**may import from it.@modelcontextprotocol/sdkimports are confined tosrc/mcp/client.tsandsrc/mcp/adapter.ts.
Architectural correctness includes key runtime invariants.
- Security must use policy snapshots, not ambient
process.cwd()/process.env. - Production code must not use mutable global singleton tool registry (
defaultRegistry).
Keep side effects behind intended layers.
- UI must not import network SDKs directly.
- UI must not import Node networking/child-process modules directly.
console.*usage is restricted to files that run before the TUI mounts (startup and MCP connection surface). The canonical allowlist is in theconsole.*rule inmodularity.test.ts; additions require the same pre-TUI justification documented there.process.stdout.write/process.stderr.write(andcork/uncork) follow the same boundary asconsole.*— the canonical allowlist is inio-boundaries.test.tsand additionally permits the headless protocol surface (src/ui/headless/**), the pre-TUI splash (src/ui/logo.ts), debug channels (src/utils/debug.ts,src/providers/instrument.ts), and last-resort writes from the session-log writer.process.exitis restricted to process-lifetime surfaces:src/index.ts,src/cli/**startup and phase files, and the headless/TUI exit sites undersrc/ui/**. Phase code must throw rather than exit; seesrc/cli/startup/AGENTS.md.
Prevent long-term drift.
- No cycles in
src/**. - Provider classes must be registered in
src/providers/registry.ts. - Do not introduce external CLI argument parser libraries.
- No
export *(orexport type *) re-exports undersrc/**. Wildcards silently widen the public surface and defeat the seam allowlists in §B; list named exports instead.
Rules that encode a specific past bug. Each names the commit that motivated it. Removing a regression contract requires demonstrating that the structural fix it guards is now enforced by the type system or a stronger rule.
- cf880ed — Provider mint + capability read must include priming (
prime/listModels/primeModelCache). The primary gate is theUnprimedProviderreturn type fromcreateProvider; this rule is belt-and-braces. - f848472 — Config read-modify-write must use
updateGlobalConfig, not a bareloadGlobalConfig+saveGlobalConfigpair. - 44aeb26 —
status-bar.tsxmust read context fullness only viacontextFillTokens; direct.totalTokens/.completionTokens/.reasoningTokensfield access is forbidden. - 550f093 — The
{ provider, model, keyId }DTO shape must not be re-declared outsidesrc/core/selection/types.ts; alias or extendModelSelection. - Shared event-render helpers (
describeRotationReason,fingerprintLabel,formatHookDisplay) must live only insrc/ui/agent-events/render.ts.
Any exception to a rule must include:
- Why it is currently necessary
- Where it is tracked (issue/TODO)
- Exit condition (what change removes it)
- Owner responsible for removal
- No silent allowlist additions.
- No broad wildcard exceptions when a file-level exception is possible.
- New exception must be mentioned in PR description under an “Architecture Exception” heading.
Architecture tests are required checks.
Minimum CI gate:
npm run test:unit:rest(includestest/unit/arch/**/*.test.ts)
Explicit arch gate:
npm run test:unit:arch— runs onlytest/unit/arch/**/*.test.ts. Should be marked as a required status check in CI.
When adding a new cross-layer behavior:
- Update
ARCHITECTURE.md(intent) - Add/adjust architecture test (enforcement)
- Update this file if policy category changes
- Merge all three in one PR
When fixing a violation:
- Prefer changing code over changing rule
- If rule must change, explain architectural rationale
- Remove stale comments/TODOs tied to old allowlists
- Does this introduce a new dependency edge across top-level folders?
- If yes, is it through an approved seam file?
- Did any architecture allowlist grow?
- If allowlist grew, is there a tracked removal plan + owner?
- If allowlist grew, is it mentioned in the PR description under an "Architecture Exception" heading?
- Do architecture tests still pass locally?
- Are docs (
ARCHITECTURE.md/ this file) updated if behavior changed?
Architecture rules live in test/unit/arch/, split per category with shared helpers in _helpers.ts:
layers.test.ts— §Aboundary-surfaces.test.ts— §B (includes §F contracts cf880ed and 550f093)runtime-contracts.test.ts— §C (includes §F contracts f848472 and 44aeb26)io-boundaries.test.ts— §Dstructural-hygiene.test.ts— §E (includes the §F render.ts-helpers contract)
- Add allowlist metadata format (comment template with owner + removal condition).
- Add CI diff guard to flag allowlist growth automatically.
- Brand
ContextFillTokensto retire the 44aeb26 arch rule by making the wrong assignment a compile error.
- This policy does not require immediate monorepo/package split.
- This policy does not ban all duplication.
- This policy does not replace code review; it makes review safer.
Refactors are episodic. Rules are continuous.
We enforce modularity by making architecture testable, required in CI, and difficult to bypass accidentally.