# Install dependencies
bun install
# Run tests
bun test
# Run type checking
bun run typecheck
# Run architecture linting
bun run lint:architectureFamiliarize yourself with ARCHITECTURE.md to understand:
- Layered architecture
- Dependency rules
- Naming conventions
- Where to put new code
Look for similar code in the codebase and follow established patterns.
- What layer does this belong in?
- Does this create any circular dependencies?
- Is naming consistent with existing code?
Put code in the right layer:
- Pure utilities →
src/lib/ - TENEX helpers →
src/utils/ - Business logic →
src/services/ - Domain logic →
src/agents/,src/conversations/,src/tools/ - Entry points →
src/commands/,src/daemon/,src/event-handler/
Create subdirectories when you have 3+ related files:
services/
├── my-feature/
│ ├── MyFeatureService.ts
│ ├── types.ts
│ ├── helper.ts
│ └── index.ts
Services: Use Service suffix
// ✅ Good
class NotificationService { }
class EmailService { }Files: Match the class name
// NotificationService.ts
export class NotificationService { }Utilities: Use descriptive names
// nostr-entity-parser.ts
export function parseNostrEntity() { }Use @/ alias for absolute imports:
// ✅ Good
import { config } from "@/services/ConfigService";
import { formatAnyError } from "@/lib/error-formatter";
// ❌ Bad
import { config } from "../../../services/ConfigService";Import directly from service directories:
// ✅ Good
import { RAGService } from "@/services/rag";
import { RALRegistry } from "@/services/ral";
// ❌ Bad (avoid barrel imports)
import { RAGService, RALRegistry } from "@/services";Declare dependencies explicitly:
export class MyService {
constructor(
private readonly config: typeof config,
private readonly logger: typeof logger
) {}
}
// Export convenience instance
export const myService = new MyService(config, logger);Check dependency direction:
lib/→ No TENEX importsutils/→ Can importlib/services/→ Can importutils/,lib/,nostr/,llm/commands/→ Can importservices/and below
Tests live next to the code they test:
services/
├── my-feature/
│ ├── MyFeatureService.ts
│ ├── __tests__/
│ │ └── MyFeatureService.test.ts
│ └── index.ts
Pure utilities in lib/ should be easy to test:
import { describe, expect, it } from "bun:test";
import { toKebabCase } from "@/lib/string";
describe("toKebabCase", () => {
it("converts PascalCase to kebab-case", () => {
expect(toKebabCase("HelloWorld")).toBe("hello-world");
});
});Use dependency injection for testability:
import { describe, expect, it } from "bun:test";
import { MyService } from "../MyService";
describe("MyService", () => {
it("does something", () => {
const mockConfig = { get: () => "test-value" };
const mockLogger = { info: () => {} };
const service = new MyService(mockConfig, mockLogger);
expect(service.doSomething()).toBe("expected-result");
});
});Follow conventional commits:
feat: add notification service
fix: resolve circular dependency in lib/fs
refactor: simplify worktree metadata handling
docs: update architecture guide
test: add tests for RAG service
Our pre-commit hook automatically runs bun run lint:architecture to enforce architectural consistency. This check validates:
- No Circular Dependencies: Ensures that our layered architecture is respected.
- Layer Boundaries: Verifies that lower layers do not import from higher layers (e.g.,
lib/cannot import fromservices/). - Naming Conventions: Checks for consistent service and file naming.
If your commit is blocked:
- Read the error message carefully. The architecture linter provides specific feedback on which files and imports are causing the violation.
- Fix the architectural issue. This usually involves moving code to the correct layer or refactoring to avoid improper dependencies.
- Consult ARCHITECTURE.md if you are unsure where a piece of code belongs.
- If you believe the hook is incorrect, ask for a second opinion in your pull request.
bun run lint:architecture- ✅
lib/has no imports fromutils/orservices/ - ✅ No circular dependencies
- ✅ Service naming conventions
- ✅ Import patterns
If it's pure (no TENEX deps):
// src/lib/my-util.ts
export function myUtil(input: string): string {
return input.toUpperCase();
}If it's TENEX-specific:
// src/utils/my-helper.ts
import { formatAnyError } from "@/lib/error-formatter";
export function myHelper(agent: AgentInstance): string {
// ... TENEX-specific logic
}1. Create service directory:
mkdir -p src/services/my-feature2. Create service file:
// src/services/my-feature/MyFeatureService.ts
import { config } from "@/services/ConfigService";
import { logger } from "@/utils/logger";
export class MyFeatureService {
constructor(
private readonly config: typeof config,
private readonly logger: typeof logger
) {}
doSomething(): void {
this.logger.info("Doing something");
}
}
export const myFeatureService = new MyFeatureService(config, logger);3. Create types file:
// src/services/my-feature/types.ts
export interface MyFeatureConfig {
enabled: boolean;
}4. Create index.ts:
// src/services/my-feature/index.ts
export { MyFeatureService, myFeatureService } from "./MyFeatureService";
export type { MyFeatureConfig } from "./types";5. Use in other code:
import { myFeatureService } from "@/services/my-feature";
myFeatureService.doSomething();// src/tools/implementations/my_tool.ts
import type { ExecutionContext } from "@/agents/execution/types";
import { tool } from "ai";
import { z } from "zod";
const myToolSchema = z.object({
input: z.string().describe("The input to process"),
});
async function executeMyTool(
input: z.infer<typeof myToolSchema>,
context: ExecutionContext
): Promise<{ result: string }> {
// Implementation
return { result: "success" };
}
export const my_tool = tool({
description: "Does something useful",
parameters: myToolSchema,
execute: async (input, { context }) => {
return await executeMyTool(input, context);
},
});Always leave the code better than you found it.
When working in a file, take the opportunity to make small improvements:
- Fix typos or unclear comments.
- Improve variable names for clarity.
- Move misplaced functions to their correct architectural layer.
- Update imports to use the
@/alias. - Resolve any nearby TODOs if they are trivial.
These small, incremental improvements are crucial for maintaining the long-term health and quality of the codebase. Every contribution, no matter how small, is an opportunity to improve our collective environment.
- Check ARCHITECTURE.md first
- Search for similar code in the codebase
- Ask in PR review
- Consult with team
The hook uses Claude Code to review commits. If blocked:
- Read the feedback carefully
- Fix the architectural issue
- Check if you're introducing circular dependencies
- Verify you're adding code to the right layer
The hook is strict but helpful - it prevents technical debt.
Before submitting:
- Tests pass:
bun test - Types check:
bun run typecheck - Architecture lint passes:
bun run lint:architecture - Pre-commit hook passes (commit locally first)
- Code follows naming conventions
- No circular dependencies introduced
- Code is in the correct layer
- Documentation updated if needed
When refactoring legacy code:
- Don't change everything at once
- Rename files first (update imports)
- Rename classes in separate PR
- Add DI gradually
- Group into subdirectories when beneficial
As this project is in active development and pre-release, we do not guarantee backward compatibility. We prioritize code quality, architectural integrity, and innovation over maintaining legacy interfaces.
Guidelines for making breaking changes:
- Do not hesitate to refactor if it improves the codebase.
- Update all relevant documentation in the same pull request.
- Communicate significant changes to the team to ensure everyone is aligned.
If you're unsure about anything:
- Check ARCHITECTURE.md
- Look for similar patterns in the codebase
- Ask in PR review
- Trust the pre-commit hook feedback
When in doubt, ask!