- Core Principles
- Layered Architecture
- Module Organization
- Naming Conventions
- Import Patterns
- Adding New Code
- Anti-Patterns to Avoid
Dependencies flow downward only, never upward:
commands/daemon/event-handler → services/agents/conversations/tools
↓
llm/nostr/prompts/events
↓
utils
↓
lib
Rule: Lower layers never import from higher layers.
The lib/ layer contains zero TENEX-specific code. These are pure, reusable utilities that could work in any Node.js project:
- Filesystem operations
- String manipulation
- Time formatting
- Validation helpers
- Error formatting
Rule: lib/ has NO imports from utils/, services/, or any TENEX modules.
The utils/ layer contains helpers specific to TENEX's domain:
- Nostr entity parsing
- Phase management
- Git operations
- Conversation utilities
Rule: utils/ can import from lib/ but not from services/ or higher layers.
The services/ layer contains stateful business logic and domain services:
- Configuration management
- RAG operations
- Scheduling
- Delegation/RAL state tracking
- MCP integration
Rule: Services can import from utils/, lib/, nostr/, llm/, but not from commands/, daemon/, or event-handler/.
Purpose: Framework-agnostic utilities
Contains:
lib/fs/- Filesystem operationslib/string.ts- String utilitieslib/error-formatter.ts- Error formattinglib/time.ts- Time utilities
Dependencies: Node.js built-ins, npm packages only
Example:
// ✅ Good - pure utility
export function toKebabCase(str: string): string {
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
}
// ❌ Bad - depends on TENEX code
import { config } from "@/services/ConfigService";Purpose: Domain-specific helpers
Contains:
utils/nostr-entity-parser.ts- Nostr parsing utilitiesutils/git/- Git operations (including worktree management)utils/phase-utils.ts- Phase managementutils/conversation-id.ts- Conversation identifier helpersutils/logger.ts- Logging (can depend on services/config)
Dependencies: lib/, Node.js built-ins, npm packages
Example:
// ✅ Good - TENEX-specific helper using lib utilities
import { formatAnyError } from "@/lib/error-formatter";
export function parseNostrUser(input: string): { pubkey: string } | null {
// ... implementation
}Modules: events/, nostr/, llm/, prompts/
Purpose: Protocol implementations and provider abstractions
Dependencies: utils/, lib/
Modules: services/, agents/, conversations/, tools/
Purpose: Business logic, state management, capabilities
Dependencies: Everything below (layers 0-2)
Modules: commands/, daemon/, event-handler/
Purpose: CLI, runtime orchestration, event routing
Dependencies: Everything below (layers 0-3)
Services should be organized by domain, with related code grouped together:
services/
├── dispatch/ # Chat routing + delegation dispatch
│ ├── AgentDispatchService.ts
│ ├── AgentRouter.ts
│ └── DelegationCompletionHandler.ts
├── ral/ # Delegation/RAL state
│ ├── RALRegistry.ts
│ ├── types.ts
│ └── index.ts
├── rag/ # RAG domain
│ ├── RAGService.ts
│ ├── RAGDatabaseService.ts
│ ├── RagSubscriptionService.ts
│ ├── ...
│ └── index.ts
├── projects/
├── scheduling/
├── reports/
├── mcp/
└── ...
When to create a subdirectory:
- 3+ related files
- Distinct domain boundary
- Internal implementation details to hide
Small services (1-2 files): Keep at top level until they grow.
Standalone developer tooling or auxiliary CLIs should live under tools/ at the repo root. Treat each tool as isolated (own package.json/tsconfig.json) and document additions in MODULE_INVENTORY.md.
Preferred suffix: Service
// ✅ Preferred
ConfigService
RAGService
SchedulerService
ProjectStatusService
// ✅ Most business-logic classes use the "Service" suffix.Goal: Consistent "Service" suffix for all business logic classes.
Exceptions: Registries (e.g., RALRegistry) keep their names when they are not business-logic services.
- Services:
SomethingService.ts(PascalCase) - Utilities:
kebab-case.tsorcamelCase.ts(be consistent within a directory) - Types:
types.tsorSomething.types.ts - Tests:
Something.test.ts(co-located in__tests__/)
Do NOT use services/index.ts barrel export. Import directly from service directories:
// ✅ Good - direct import
import { RALRegistry } from "@/services/ral";
import { RAGService } from "@/services/rag";
// ❌ Bad - barrel import
import { RALRegistry, RAGService } from "@/services";Why:
- Explicit dependencies
- Better tree-shaking
- Faster TypeScript compilation
- No barrel maintenance
Each service subdirectory has an index.ts that controls what's public:
// services/ral/index.ts
export { RALRegistry } from "./RALRegistry";
export type { PendingDelegation, CompletedDelegation } from "./types";Always use the @/ path alias for absolute imports:
// ✅ Good
import { config } from "@/services/ConfigService";
import { formatAnyError } from "@/lib/error-formatter";
// ❌ Bad - relative imports for cross-module
import { config } from "../../../services/ConfigService";1. Determine if it's pure or TENEX-specific:
- Pure (no TENEX deps) →
lib/ - TENEX-specific →
utils/
2. Create the file:
// lib/array-utils.ts (pure utility)
export function chunk<T>(array: T[], size: number): T[][] {
// ... implementation
}3. Prefer direct imports. Add an index.ts only when a subdirectory needs an explicit public surface.
1. Decide if it needs a subdirectory:
- Yes if: 3+ related files, distinct domain
- No if: 1-2 files, simple service
2. Create the service:
// services/notifications/NotificationService.ts
import { config } from "@/services/ConfigService";
import { logger } from "@/utils/logger";
export class NotificationService {
constructor(
private readonly config: typeof config,
private readonly logger: typeof logger
) {}
// Methods...
}
// Export default instance for convenience
export const notificationService = new NotificationService(config, logger);3. Create index.ts:
// services/notifications/index.ts
export { NotificationService, notificationService } from "./NotificationService";
export type { NotificationOptions } from "./types";4. Import where needed:
import { notificationService } from "@/services/notifications";Adding to lib/:
- Must be pure, no TENEX dependencies
- No imports from
utils/,services/, etc. - Use console.error instead of TENEX logger
Adding to utils/:
- Can import from
lib/ - Should be domain helpers, not business logic
- If it needs state, it should be a service instead
Adding to services/:
- Can import from
utils/,lib/,nostr/,llm/,prompts/,events/ - Cannot import from
commands/,daemon/,event-handler/
// lib/something.ts
import { logger } from "@/utils/logger"; // ❌ lib → utils (wrong direction)
// utils/helper.ts
import { someService } from "@/services/SomeService"; // ❌ utils → servicesSolution: Move code to correct layer or use dependency injection.
// ❌ Importing from barrel when subdirectory exists
import { RAGService } from "@/services";
// ✅ Import directly from subdirectory
import { RAGService } from "@/services/rag";// ❌ Bad - stateful service masquerading as utility
// utils/user-manager.ts
export class UserManager {
private users: Map<string, User> = new Map();
// ... state management
}Solution: Move to services/ if it has state or business logic.
Avoid mixing naming styles for the same layer. Use Service for business logic, and reserve Registry/Manager for registries or infrastructure helpers.
Recommended pattern for services:
// services/something/SomethingService.ts
export class SomethingService {
// Declare dependencies in constructor
constructor(
private readonly config: typeof config,
private readonly logger: typeof logger,
private readonly someOtherService: SomeOtherService
) {}
doSomething(): void {
// Use injected dependencies
const value = this.config.getConfigPath();
this.logger.info("Doing something", { value });
}
}
// Export default instance for convenience
import { config } from "@/services/ConfigService";
import { logger } from "@/utils/logger";
import { someOtherService } from "@/services/some-other";
export const somethingService = new SomethingService(
config,
logger,
someOtherService
);Benefits:
- Testable (inject mocks)
- Clear dependencies
- No hidden singletons
- Convenient default instance
- Pure Utilities in
lib/: All pure, framework-agnostic utilities are now isolated in thelib/directory with zero TENEX dependencies. - No Circular Dependencies: All circular dependencies between layers have been resolved.
- Consistent Service Naming: All services have been refactored to use the
Servicesuffix, removing legacy names likeReportManagerandPubkeyNameRepository. - Git Utilities Consolidated: All Git-related helpers, including worktree management, are now centralized in
utils/git/. - Configuration Architecture: A centralized
ConfigServicenow manages all configuration, ensuring consistent and predictable settings management.
We are incrementally moving toward:
- ⏳ Subdirectory Grouping for Services: Gradually group related services into subdirectories for better organization.
- ⏳ Dependency Injection Pattern: Continue to adopt dependency injection for all services to improve testability and clarity.
- ⏳ Removal of Barrel Exports: Phase out all remaining barrel exports in favor of direct imports.
Philosophy: Make incremental improvements. Leave code better than you found it (Boy Scout Rule).
If unsure where code belongs:
- Is it pure/framework-agnostic? →
lib/ - Is it a TENEX helper with no state? →
utils/ - Does it manage state or business logic? →
services/ - Is it protocol-specific? →
nostr/,llm/, etc.
When in doubt, ask in PR review or check this document.
- CONTRIBUTING.md - Development workflow
- MODULE_INVENTORY.md - Current module list
- TESTING_STATUS.md - Testing guidelines